本文搬運(yùn)自:https://www.infoq.cn/article/4TCTAeoyvLNK95iuhgom
與數(shù)組相比,記錄數(shù)據(jù)結(jié)構(gòu)中的字段數(shù)目固定概而,每個都有一個名稱呼巷,類型也可以不同。
本文將介紹 Python 中的記錄赎瑰、結(jié)構(gòu)體王悍,以及“純數(shù)據(jù)對象”,但只介紹標(biāo)準(zhǔn)庫中含有的內(nèi)置數(shù)據(jù)類型和類餐曼。
順便說一句压储,這里的“記錄”定義很寬泛。例如源譬,這里也會介紹像Python 的內(nèi)置元組這樣的類型集惋。由于元組中的字段沒有名稱,因此一般不認(rèn)為它是嚴(yán)格意義上的記錄踩娘。
Python 提供了幾種可用于實(shí)現(xiàn)記錄刮刑、結(jié)構(gòu)體和數(shù)據(jù)傳輸對象的數(shù)據(jù)類型。本節(jié)將快速介紹每個實(shí)現(xiàn)及各自特性养渴,最后進(jìn)行總結(jié)并給出一個決策指南雷绢,用來幫你做出自己的選擇。
好吧厚脉,讓我們開始吧习寸!
字典——簡單數(shù)據(jù)對象
Python 字典能存儲任意數(shù)量的對象,每個對象都由唯一的鍵來標(biāo)識傻工。字典也常常稱為映射或關(guān)聯(lián)數(shù)組霞溪,能高效地根據(jù)給定的鍵查找、插入和刪除所關(guān)聯(lián)的對象中捆。
Python 的字典還可以作為記錄數(shù)據(jù)類型(record data type)或數(shù)據(jù)對象來使用鸯匹。在 Python 中創(chuàng)建字典很容易,因?yàn)檎Z言內(nèi)置了創(chuàng)建字典的語法糖泄伪,簡潔又方便殴蓬。
字典創(chuàng)建的數(shù)據(jù)對象是可變的,同時由于可以隨意添加和刪除字段,因此對字段名稱幾乎沒有保護(hù)措施染厅。這些特性綜合起來可能會引入令人驚訝的 bug痘绎,畢竟要在便利性和避免錯誤之間做出取舍。
car1 = {
'color': 'red',
'mileage': 3812.4,
'automatic': True,
}
car2 = {
'color': 'blue',
'mileage': 40231,
'automatic': False,
}
# 字典有不錯的 __repr__ 方法:
car2
{'color': 'blue', 'automatic': False, 'mileage': 40231}
# 獲取 mileage:
car2['mileage']
40231
# 字典是可變的:
car2['mileage'] = 12
car2['windshield'] = 'broken'
car2
{'windshield': 'broken', 'color': 'blue',
'automatic': False, 'mileage': 12}
# 對于提供錯誤肖粮、缺失和額外的字段名稱并沒有保護(hù)措施:
car3 = {
'colr': 'green',
'automatic': False,
'windshield': 'broken',
}
元組——不可變對象集合
Python 元組是簡單的數(shù)據(jù)結(jié)構(gòu)孤页,用于對任意對象進(jìn)行分組。元組是不可變的涩馆,創(chuàng)建后無法修改行施。
在性能方面,元組占用的內(nèi)存略少于 CPython 中的列表魂那,構(gòu)建速度也更快蛾号。
從如下反匯編的字節(jié)碼中可以看到,構(gòu)造元組常量只需要一個 LOAD_CONST 操作碼涯雅,而構(gòu)造具有相同內(nèi)容的列表對象則需要多個操作:
import dis
dis.dis(compile("(23, 'a', 'b', 'c')", '', 'eval'))
0 LOAD_CONST 4 ((23, 'a', 'b', 'c'))
3 RETURN_VALUE
dis.dis(compile("[23, 'a', 'b', 'c']", '', 'eval'))
0 LOAD_CONST 0 (23)
3 LOAD_CONST 1 ('a')
6 LOAD_CONST 2 ('b')
9 LOAD_CONST 3 ('c')
12 BUILD_LIST 4
15 RETURN_VALUE
不過你無須過分關(guān)注這些差異鲜结。在實(shí)踐中這些性能差異通常可以忽略不計(jì)斩芭,試圖通過用元組替換列表來獲得額外的性能提升一般都是入了歧途轻腺。
單純的元組有一個潛在缺點(diǎn),即存儲在其中的數(shù)據(jù)只能通過整數(shù)索引來訪問划乖,無法為元組中存儲的單個屬性制定一個名稱贬养,從而影響了代碼的可讀性。
此外琴庵,元組總是一個單例模式的結(jié)構(gòu)误算,很難確保兩個元組存儲了相同數(shù)量的字段和相同的屬性。
這樣很容易因疏忽而犯錯迷殿,比如弄錯字段順序儿礼。因此,建議盡可能減少元組中存儲的字段數(shù)量庆寺。
# 字段:color蚊夫、mileage、automatic
car1 = ('red', 3812.4, True)
car2 = ('blue', 40231.0, False)
# 元組的實(shí)例有不錯的 __repr__ 方法:
car1
('red', 3812.4, True)
car2
('blue', 40231.0, False)
# 獲取 mileage:
car2[1]
40231.0
# 元組是可變的:
car2[1] = 12
TypeError:
"'tuple' object does not support item assignment"
# 對于錯誤或額外的字段懦尝,以及提供錯誤的字段順序知纷,并沒有報錯措施:
car3 = (3431.5, 'green', True, 'silver')
編寫自定義類——手動精細(xì)控制
類可用來為數(shù)據(jù)對象定義可重用的“藍(lán)圖”(blueprint),以確保每個對象都提供相同的字段陵霉。
普通的 Python 類可作為記錄數(shù)據(jù)類型琅轧,但需要手動完成一些其他實(shí)現(xiàn)中已有的便利功能。例如踊挠,向 init 構(gòu)造函數(shù)添加新字段就很煩瑣且耗時乍桂。
此外,對于從自定義類實(shí)例化得到的對象,其默認(rèn)的字符串表示形式?jīng)]什么用睹酌。解決這個問題需要添加自己的 repr 方法权谁。這個方法通常很冗長,每次添加新字段時都必須更新忍疾。
存儲在類上的字段是可變的闯传,并且可以隨意添加新字段。使用 @property 裝飾器能創(chuàng)建只讀字段卤妒,并獲得更多的訪問控制,但是這又需要編寫更多的膠水代碼字币。
編寫自定義類適合將業(yè)務(wù)邏輯和行為添加到記錄對象中则披,但這意味著這些對象在技術(shù)上不再是普通的純數(shù)據(jù)對象。
class Car:
def __init__(self, color, mileage, automatic):
self.color = color
self.mileage = mileage
self.automatic = automatic
car1 = Car('red', 3812.4, True)
car2 = Car('blue', 40231.0, False)
# 獲取 mileage:
car2.mileage
40231.0
# 類是可變的:
car2.mileage = 12
car2.windshield = 'broken'
# 類的默認(rèn)字符串形式?jīng)]多大用處洗出,必須手動編寫一個 __repr__ 方法:
car1
<Car object at 0x1081e69e8>
collections.namedtuple——方便的數(shù)據(jù)對象
自 Python 2.6 以來添加的 namedtuple 類擴(kuò)展了內(nèi)置元組數(shù)據(jù)類型士复。與自定義類相似,namedtuple 可以為記錄定義可重用的“藍(lán)圖”翩活,以確保每次都使用正確的字段名稱阱洪。
與普通的元組一樣,namedtuple 是不可變的菠镇。這意味著在創(chuàng)建 namedtuple 實(shí)例之后就不能再添加新字段或修改現(xiàn)有字段冗荸。
除此之外,namedtuple 就相當(dāng)于具有名稱的元組利耍。存儲在其中的每個對象都可以通過唯一標(biāo)識符訪問蚌本。因此無須整數(shù)索引,也無須使用變通方法隘梨,比如將整數(shù)常量定義為索引的助記符程癌。
namedtuple 對象在內(nèi)部是作為普通的 Python 類實(shí)現(xiàn)的,其內(nèi)存占用優(yōu)于普通的類轴猎,和普通元組一樣高效:
>>> from collections import namedtuple
>>> from sys import getsizeof
>>> p1 = namedtuple('Point', 'x y z')(1, 2, 3)
>>> p2 = (1, 2, 3)
>>> getsizeof(p1)
72
>>> getsizeof(p2)
72
由于使用 namedtuple 就必須更好地組織數(shù)據(jù)嵌莉,因此無意中清理了代碼并讓其更加易讀。
我發(fā)現(xiàn)從專用的數(shù)據(jù)類型(例如固定格式的字典)切換到 namedtuple 有助于更清楚地表達(dá)代碼的意圖捻脖。通常锐峭,每當(dāng)我在用 namedtuple 重構(gòu)應(yīng)用時,都神奇地為代碼中的問題想出了更好的解決辦法郎仆。
用 namedtuple 替換普通(非結(jié)構(gòu)化的)元組和字典還可以減輕同事的負(fù)擔(dān)只祠,因?yàn)橛?namedtuple 傳遞的數(shù)據(jù)在某種程度上能做到“自說明”。
>>> from collections import namedtuple
>>> Car = namedtuple('Car' , 'color mileage automatic')
>>> car1 = Car('red', 3812.4, True)
# 實(shí)例有不錯的 __repr__ 方法:
>>> car1
Car(color='red', mileage=3812.4, automatic=True)
# 訪問字段:
>>> car1.mileage
3812.4
# 字段是不可變的:
>>> car1.mileage = 12
AttributeError: "can't set attribute"
>>> car1.windshield = 'broken'
AttributeError:
"'Car' object has no attribute 'windshield'"
typing.NamedTuple——改進(jìn)版 namedtuple
這個類添加自 Python 3.6扰肌,是 collections 模塊中 namedtuple 類的姊妹抛寝。它與 namedtuple 非常相似,主要區(qū)別在于用新語法來定義記錄類型并支持類型注解(type hint)。
注意盗舰,只有像 mypy 這樣獨(dú)立的類型檢查工具才會在意類型注解晶府。不過即使沒有工具支持,類型注解也可幫助其他程序員更好地理解代碼(如果類型注解沒有隨代碼及時更新則會帶來混亂)钻趋。
>>> from typing import NamedTuple
class Car(NamedTuple):
color: str
mileage: float
automatic: bool
>>> car1 = Car('red', 3812.4, True)
# 實(shí)例有不錯的 __repr__ 方法:
>>> car1
Car(color='red', mileage=3812.4, automatic=True)
# 訪問字段:
>>> car1.mileage
3812.4
# 字段是不可變的:
>>> car1.mileage = 12
AttributeError: "can't set attribute"
>>> car1.windshield = 'broken'
AttributeError:
"'Car' object has no attribute 'windshield'"
# 只有像 mypy 這樣的類型檢查工具才會落實(shí)類型注解:
>>> Car('red', 'NOT_A_FLOAT', 99)
Car(color='red', mileage='NOT_A_FLOAT', automatic=99)
struct.Struct——序列化 C 結(jié)構(gòu)體
struct.Struct 類用于在 Python 值和 C 結(jié)構(gòu)體之間轉(zhuǎn)換川陆,并將其序列化為 Python 字節(jié)對象。例如可以用來處理存儲在文件中或來自網(wǎng)絡(luò)連接的二進(jìn)制數(shù)據(jù)蛮位。
結(jié)構(gòu)體使用與格式化字符串類似的語法來定義较沪,能夠定義并組織各種 C 數(shù)據(jù)類型(如 char、int失仁、long尸曼,以及對應(yīng)的無符號的變體)。
序列化結(jié)構(gòu)體一般不用來表示只在 Python 代碼中處理的數(shù)據(jù)對象萄焦,而是主要用作數(shù)據(jù)交換格式控轿。
在某些情況下,與其他數(shù)據(jù)類型相比拂封,將原始數(shù)據(jù)類型打包到結(jié)構(gòu)體中占用的內(nèi)存較少茬射。但大多數(shù)情況下這都屬于高級(且可能不必要的)優(yōu)化。
>>> from struct import Struct
>>> MyStruct = Struct('i?f')
>>> data = MyStruct.pack(23, False, 42.0)
# 得到的是一團(tuán)內(nèi)存中的數(shù)據(jù):
>>> data
b'\x17\x00\x00\x00\x00\x00\x00\x00\x00\x00(B'
# 數(shù)據(jù)可以再次解包:
>>> MyStruct.unpack(data)
(23, False, 42.0)
types.SimpleNamespace——花哨的屬性訪問
這里再介紹一種高深的方法來在 Python 中創(chuàng)建數(shù)據(jù)對象:types.SimpleNamespace冒签。該類添加自 Python 3.3在抛,可以用屬性訪問的方式訪問其名稱空間。
也就是說镣衡,SimpleNamespace 實(shí)例將其中的所有鍵都公開為類屬性霜定。因此訪問屬性時可以使用 obj.key 這樣的點(diǎn)式語法,不需要用普通字典的 obj[‘key’] 方括號索引語法廊鸥。所有實(shí)例默認(rèn)都包含一個不錯的 repr望浩。
正如其名,SimpleNamespace 很簡單惰说,基本上就是擴(kuò)展版的字典磨德,能夠很好地訪問屬性并以字符串打印出來,還能自由地添加吆视、修改和刪除屬性典挑。
>>> from types import SimpleNamespace
>>> car1 = SimpleNamespace(color='red',
... mileage=3812.4,
... automatic=True)
# 默認(rèn)的 __repr__ 效果:
>>> car1
namespace(automatic=True, color='red', mileage=3812.4)
# 實(shí)例支持屬性訪問并且是可變的:
>>> car1.mileage = 12
>>> car1.windshield = 'broken'
>>> del car1.automatic
>>> car1
namespace(color='red', mileage=12, windshield='broken')
小結(jié)
那么在 Python 中應(yīng)該使用哪種類型的數(shù)據(jù)對象呢?從上面可以看到啦吧,Python 中有許多不同的方法實(shí)現(xiàn)記錄或數(shù)據(jù)對象您觉,使用哪種方式通常取決于具體的情況。
如果只有兩三個字段授滓,字段順序易于記憶或無須使用字段名稱琳水,則使用簡單元組對象肆糕。例如三維空間中的 (x, y, z) 點(diǎn)。
如果需要實(shí)現(xiàn)含有不可變字段的數(shù)據(jù)對象在孝,則使用 collections.namedtuple 或 typing.NamedTuple 這樣的簡單元組诚啃。
如果想鎖定字段名稱來避免輸入錯誤,同樣建議使用 collections.namedtuple 和 typing.NamedTuple私沮。
如果希望保持簡單始赎,建議使用簡單的字典對象,其語法方便仔燕,和 JSON 也類似造垛。
如果需要對數(shù)據(jù)結(jié)構(gòu)完全掌控,可以用 @property 加上設(shè)置方法和獲取方法來編寫自定義的類涨享。
如果需要向?qū)ο筇砑有袨椋ǚ椒ǎ┙畈瑒t應(yīng)該從頭開始編寫自定義類,或者通過擴(kuò)展 collections.namedtuple 或 typing.NamedTuple 來編寫自定義類厕隧。
如果想嚴(yán)格打包數(shù)據(jù)以將其序列化到磁盤上或通過網(wǎng)絡(luò)發(fā)送,建議使用 struct.Struct俄周。
一般情況下吁讨,如果想在 Python 中實(shí)現(xiàn)一個普通的記錄、結(jié)構(gòu)體或數(shù)據(jù)對象峦朗,我的建議是在{\rm Python}~2.x 中使用 collections.namedtuple建丧,在 Python 3 中使用其姊妹 typing.NamedTuple。