最全的命名元組Namedtuple使用指南?。?!

劇照 | 《鬼滅之刃》
原文地址:https://miguendes.me/everything-you-need-to-know-about-pythons-namedtuples
作者:Miguel Brito
本文將討論 python 中namedtuple的幾個重點(diǎn)用法。我們將由淺入深的介紹namedtuple的各概念。你將了解為什么使用它們,以及如何使用它們,從而使代碼更簡潔。
在學(xué)習(xí)本指南之后,你一定會喜歡上使用它。
學(xué)習(xí)目標(biāo)
在本教程結(jié)束時,你應(yīng)該能夠:
了解為什么以及何時使用 Namedtuple將常規(guī)元組和字典轉(zhuǎn)換為 Namedtuple將 Namedtuple轉(zhuǎn)化為字典或常規(guī)元組對 Namedtuple列表進(jìn)行排序了解 Namedtuple和數(shù)據(jù)類(DataClass)之間的區(qū)別使用可選字段創(chuàng)建 Namedtuple將 Namedtuple序列化為 JSON添加文檔字符串(docstring)
為什么要使用namedtuple?
namedtuple是一個非常有趣(也被低估了)的數(shù)據(jù)結(jié)構(gòu)。我們可以輕松找到嚴(yán)重依賴常規(guī)元組和字典來存儲數(shù)據(jù)的 Python 代碼。我并不是說,這樣不好,只是有時候他們常常被濫用,且聽我慢慢道來。
假設(shè)你有一個將字符串轉(zhuǎn)換為顏色的函數(shù)。顏色必須在 4 維空間 RGBA 中表示。
def?convert_string_to_color(desc:?str,?alpha:?float?=?0.0):
????if?desc?==?"green":
????????return?50,?205,?50,?alpha
????elif?desc?==?"blue":
????????return?0,?0,?255,?alpha
????else:
????????return?0,?0,?0,?alpha
然后,我們可以像這樣使用它:
r,?g,?b,?a?=?convert_string_to_color(desc="blue",?alpha=1.0)
好的,可以。但是我們這里有幾個問題。第一個是,無法確保返回值的順序。也就是說,沒有什么可以阻止其他開發(fā)者這樣調(diào)用
convert_string_to_color:
g,?b,?r,?a?=?convert_string_to_color(desc="blue",?alpha=1.0)
另外,我們可能不知道該函數(shù)返回 4 個值,可能會這樣調(diào)用該函數(shù):
r,?g,?b?=?convert_string_to_color(desc="blue",?alpha=1.0)
于是,因?yàn)榉祷刂蒂x值失敗,拋出ValueError錯誤,調(diào)用失敗。
確實(shí)如此。但是,你可能會問,為什么不使用字典呢?
Python 的字典是一種非常通用的數(shù)據(jù)結(jié)構(gòu)。它們是一種存儲多個值的簡便方法。但是,字典并非沒有缺點(diǎn)。由于其靈活性,字典很容易被濫用。讓 我們看看使用字典之后的例子。
def?convert_string_to_color(desc:?str,?alpha:?float?=?0.0):
????if?desc?==?"green":
????????return?{"r":?50,?"g":?205,?"b":?50,?"alpha":?alpha}
????elif?desc?==?"blue":
????????return?{"r":?0,?"g":?0,?"b":?255,?"alpha":?alpha}
????else:
????????return?{"r":?0,?"g":?0,?"b":?0,?"alpha":?alpha}
好的,我們現(xiàn)在可以像這樣使用它,期望只返回一個值:
color?=?convert_string_to_color(desc="blue",?alpha=1.0)
無需記住順序,但它至少有兩個缺點(diǎn)。第一個是我們必須跟蹤密鑰的名稱。如果我們將其更改{"r": 0, “g”: 0, “b”: 0, “alpha”: alpha}為{”red": 0, “green”: 0, “blue”: 0, “a”: alpha},則在訪問字段時會得到KeyError返回,因?yàn)殒Ir,g,b和alpha不再存在。
字典的第二個問題是它們不可散列。這意味著我們無法將它們存儲在 set 或其他字典中。假設(shè)我們要跟蹤特定圖像有多少種顏色。如果我們使用collections.Counter計數(shù),我們將得到TypeError: unhashable type: ‘dict’。
而且,字典是可變的,因此我們可以根據(jù)需要添加任意數(shù)量的新鍵。相信我,這是一些很難發(fā)現(xiàn)的令人討厭的錯誤點(diǎn)。
好的,很好。那么現(xiàn)在怎么辦?我可以用什么代替呢?
namedtuple!對,就是它!
將我們的函數(shù)轉(zhuǎn)換為使用namedtuple:
from?collections?import?namedtuple
...
Color?=?namedtuple("Color",?"r?g?b?alpha")
...
def?convert_string_to_color(desc:?str,?alpha:?float?=?0.0):
????if?desc?==?"green":
????????return?Color(r=50,?g=205,?b=50,?alpha=alpha)
????elif?desc?==?"blue":
????????return?Color(r=50,?g=0,?b=255,?alpha=alpha)
????else:
????????return?Color(r=50,?g=0,?b=0,?alpha=alpha)
與 dict 的情況一樣,我們可以將值分配給單個變量并根據(jù)需要使用。無需記住順序。而且,如果你使用的是諸如 PyCharm 和 VSCode 之類的 IDE ,還可以自動提示補(bǔ)全。
color?=?convert_string_to_color(desc="blue",?alpha=1.0)
...
has_alpha?=?color.alpha?>?0.0
...
is_black?=?color.r?==?0?and?color.g?==?0?and?color.b?==?0
最重要的是namedtuple是不可變的。如果團(tuán)隊中的另一位開發(fā)人員認(rèn)為在運(yùn)行時添加新字段是個好主意,則該程序?qū)箦e。
>>>?blue?=?Color(r=0,?g=0,?b=255,?alpha=1.0)
>>>?blue.e?=?0
---------------------------------------------------------------------------
AttributeError????????????????????????????Traceback?(most?recent?call?last)
-13-8c7f9b29c633>?in?
---->?1?blue.e?=?0
AttributeError:?'Color'?object?has?no?attribute?'e'
不僅如此,現(xiàn)在我們可以使用它 Counter 來跟蹤一個集合有多少種顏色。
>>>?Counter([blue,?blue])
>>>?Counter({Color(r=0,?g=0,?b=255,?alpha=1.0):?2})
如何將常規(guī)元組或字典轉(zhuǎn)換為 namedtuple
現(xiàn)在我們了解了為什么使用 namedtuple,現(xiàn)在該學(xué)習(xí)如何將常規(guī)元組和字典轉(zhuǎn)換為 namedtuple 了。假設(shè)由于某種原因,你有包含彩色 RGBA 值的字典實(shí)例。如果要將其轉(zhuǎn)換為Color namedtuple,則可以按以下步驟進(jìn)行:
>>>?c?=?{"r":?50,?"g":?205,?"b":?50,?"alpha":?alpha}
>>>?Color(**c)
>>>?Color(r=50,?g=205,?b=50,?alpha=0)
我們可以利用該**結(jié)構(gòu)將包解壓縮dict為namedtuple。
如果我想從 dict 創(chuàng)建一個 namedtupe,如何做?
沒問題,下面這樣做就可以了:
>>>?c?=?{"r":?50,?"g":?205,?"b":?50,?"alpha":?alpha}
>>>?Color?=?namedtuple("Color",?c)
>>>?Color(**c)
Color(r=50,?g=205,?b=50,?alpha=0)
通過將 dict 實(shí)例傳遞給 namedtuple 工廠函數(shù),它將為你創(chuàng)建字段。然后,Color 像上邊的例子一樣解壓字典 c,創(chuàng)建新實(shí)例。
如何將 namedtuple 轉(zhuǎn)換為字典或常規(guī)元組
我們剛剛學(xué)習(xí)了如何將轉(zhuǎn)換namedtuple為dict。反過來呢?我們又如何將其轉(zhuǎn)換為字典實(shí)例?
實(shí)驗(yàn)證明,namedtuple 它帶有一種稱為的方法._asdict()。因此,轉(zhuǎn)換它就像調(diào)用方法一樣簡單。
>>>?blue?=?Color(r=0,?g=0,?b=255,?alpha=1.0)
>>>?blue._asdict()
{'r':?0,?'g':?0,?'b':?255,?'alpha':?1.0}
你可能想知道為什么該方法以_開頭。這是與 Python 的常規(guī)規(guī)范不一致的一個地方。通常,_代表私有方法或?qū)傩?。但是?code style="font-size: 14px;overflow-wrap: break-word;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;color: rgb(30, 107, 184);background-color: rgba(27, 31, 35, 0.05);font-family: 'Operator Mono', Consolas, Monaco, Menlo, monospace;word-break: break-all;">namedtuple為了避免命名沖突將它們添加到了公共方法中。除了_asdict,還有_replace,_fields和_field_defaults。你可以在這里[1]找到所有這些。
要將namedtupe轉(zhuǎn)換為常規(guī)元組,只需將其傳遞給 tuple 構(gòu)造函數(shù)即可。
>>>?tuple(Color(r=50,?g=205,?b=50,?alpha=0.1))
(50,?205,?50,?0.1)
如何對 namedtuples 列表進(jìn)行排序
另一個常見的用例是將多個namedtuple實(shí)例存儲在列表中,并根據(jù)某些條件對它們進(jìn)行排序。例如,假設(shè)我們有一個顏色列表,我們需要按 alpha 強(qiáng)度對其進(jìn)行排序。
幸運(yùn)的是,Python 允許使用非常 Python 化的方式來執(zhí)行此操作。我們可以使用operator.attrgetter運(yùn)算符。根據(jù)文檔[2],attrgetter“返回從其操作數(shù)獲取 attr 的可調(diào)用對象”。簡單來說就是,我們可以通過該運(yùn)算符,來獲取傳遞給 sorted 函數(shù)排序的字段。例:
from?operator?import?attrgetter
...
colors?=?[
????Color(r=50,?g=205,?b=50,?alpha=0.1),
????Color(r=50,?g=205,?b=50,?alpha=0.5),
????Color(r=50,?g=0,?b=0,?alpha=0.3)
]
...
>>>?sorted(colors,?key=attrgetter("alpha"))
[Color(r=50,?g=205,?b=50,?alpha=0.1),
?Color(r=50,?g=0,?b=0,?alpha=0.3),
?Color(r=50,?g=205,?b=50,?alpha=0.5)]
現(xiàn)在,顏色列表按 alpha 強(qiáng)度升序排列!
如何將 namedtuples 序列化為 JSON
有時你可能需要將儲存namedtuple轉(zhuǎn)為 JSON。Python 的字典可以通過 json 模塊轉(zhuǎn)換為 JSON。那么我們可以使用_asdict 方法將元組轉(zhuǎn)換為字典,然后接下來就和字典一樣了。例如:
>>>?blue?=?Color(r=0,?g=0,?b=255,?alpha=1.0)
>>>?import?json
>>>?json.dumps(blue._asdict())
'{"r":?0,?"g":?0,?"b":?255,?"alpha":?1.0}'
如何給 namedtuple 添加 docstring
在 Python 中,我們可以使用純字符串來記錄方法,類和模塊。然后,此字符串可作為名為的特殊屬性使用__doc__。話雖這么說,我們?nèi)绾蜗蛭覀兊?code style="font-size: 14px;overflow-wrap: break-word;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;color: rgb(30, 107, 184);background-color: rgba(27, 31, 35, 0.05);font-family: 'Operator Mono', Consolas, Monaco, Menlo, monospace;word-break: break-all;">Color namedtuple添加 docstring 的?
我們可以通過兩種方式做到這一點(diǎn)。第一個(比較麻煩)是使用包裝器擴(kuò)展元組。這樣,我們便可以 docstring 在此包裝器中定義。例如,請考慮以下代碼片段:
_Color?=?namedtuple("Color",?"r?g?b?alpha")
class?Color(_Color):
????"""A?namedtuple?that?represents?a?color.
????It?has?4?fields:
????r?-?red
????g?-?green
????b?-?blue
????alpha?-?the?alpha?channel
????"""
>>>?print(Color.__doc__)
A?namedtuple?that?represents?a?color.
????It?has?4?fields:
????r?-?red
????g?-?green
????b?-?blue
????alpha?-?the?alpha?channel
>>>?help(Color)
Help?on?class?Color?in?module?__main__:
class?Color(Color)
?|??Color(r,?g,?b,?alpha)
?|
?|??A?namedtuple?that?represents?a?color.
?|??It?has?4?fields:
?|??r?-?red
?|??g?-?green
?|??b?-?blue
?|??alpha?-?the?alpha?channel
?|
?|??Method?resolution?order:
?|??????Color
?|??????Color
?|??????builtins.tuple
?|??????builtins.object
?|
?|??Data?descriptors?defined?here:
?|
?|??__dict__
?|??????dictionary?for?instance?variables?(if?defined)
如上,通過繼承_Color元組,我們?yōu)?namedtupe 添加了一個__doc__屬性。
添加的第二種方法,直接設(shè)置__doc__屬性。這種方法不需要擴(kuò)展元組。
>>>?Color.__doc__?=?"""A?namedtuple?that?represents?a?color.
????It?has?4?fields:
????r?-?red
????g?-?green
????b?-?blue
????alpha?-?the?alpha?channel
????"""
注意,這些方法僅適用于Python 3+。
namedtuples 和數(shù)據(jù)類(Data Class)之間有什么區(qū)別?
功能
在 Python 3.7 之前,可使用以下任一方法創(chuàng)建一個簡單的數(shù)據(jù)容器:
namedtuple 常規(guī)類 第三方庫, attrs
如果你想使用常規(guī)類,那意味著你將必須實(shí)現(xiàn)幾個方法。例如,常規(guī)類將需要一種__init__方法來在類實(shí)例化期間設(shè)置屬性。如果你希望該類是可哈希的,則意味著自己實(shí)現(xiàn)一個__hash__方法。為了比較不同的對象,還需要__eq__實(shí)現(xiàn)一個方法。最后,為了簡化調(diào)試,你需要一種__repr__方法。
讓我們使用常規(guī)類來實(shí)現(xiàn)下我們的顏色用例。
class?Color:
????"""A?regular?class?that?represents?a?color."""
????def?__init__(self,?r,?g,?b,?alpha=0.0):
????????self.r?=?r
????????self.g?=?g
????????self.b?=?b
????????self.alpha?=?alpha
????def?__hash__(self):
????????return?hash((self.r,?self.g,?self.b,?self.alpha))
????def?__repr__(self):
????????return?"{0}({1},?{2},?{3},?{4})".format(
????????????self.__class__.__name__,?self.r,?self.g,?self.b,?self.alpha
????????)
????def?__eq__(self,?other):
????????if?not?isinstance(other,?Color):
????????????return?False
????????return?(
????????????self.r?==?other.r
????????????and?self.g?==?other.g
????????????and?self.b?==?other.b
????????????and?self.alpha?==?other.alpha
????????)
如上,你需要實(shí)現(xiàn)好多方法。你只需要一個容器來為你保存數(shù)據(jù),而不必?fù)?dān)心分散注意力的細(xì)節(jié)。同樣,人們偏愛實(shí)現(xiàn)類的一個關(guān)鍵區(qū)別是常規(guī)類是可變的。
實(shí)際上,引入數(shù)據(jù)類(Data Class)的PEP[3]將它們稱為“具有默認(rèn)值的可變 namedtuple”(譯者注:Data Class python 3.7 引入,參考:https://docs.python.org/zh-cn/3/library/dataclasses.html)。
現(xiàn)在,讓我們看看如何用數(shù)據(jù)類來實(shí)現(xiàn)。
from?dataclasses?import?dataclass
...
@dataclass
class?Color:
????"""A?regular?class?that?represents?a?color."""
????r:?float
????g:?float
????b:?float
????alpha:?float
哇!就是這么簡單。由于沒有__init__,你只需在 docstring 后面定義屬性即可。此外,必須使用類型提示對其進(jìn)行注釋。
除了可變之外,數(shù)據(jù)類還可以開箱即用提供可選字段。假設(shè)我們的 Color 類不需要 alpha 字段。然后我們可以設(shè)置為可選。
from?dataclasses?import?dataclass
from?typing?import?Optional
...
@dataclass
class?Color:
????"""A?regular?class?that?represents?a?color."""
????r:?float
????g:?float
????b:?float
????alpha:?Optional[float]
我們可以像這樣實(shí)例化它:
>>> blue = Color(r=0, g=0, b=255)
由于它們是可變的,因此我們可以更改所需的任何字段。我們可以像這樣實(shí)例化它:
>>>?blue?=?Color(r=0,?g=0,?b=255)
>>>?blue.r?=?1
>>>?#?可以設(shè)置更多的屬性字段
>>>?blue.e?=?10
相較之下,namedtuple默認(rèn)情況下沒有可選字段。要添加它們,我們需要一點(diǎn)技巧和一些元編程。
提示:要添加__hash__方法,你需要通過將設(shè)置unsafe_hash為使其不可變True:
@dataclass(unsafe_hash=True)
class?Color:
????...
另一個區(qū)別是,拆箱(unpacking)是 namedtuples 的自帶的功能(first-class citizen)。如果希望數(shù)據(jù)類具有相同的行為,則必須實(shí)現(xiàn)自己。
from?dataclasses?import?dataclass,?astuple
...
@dataclass
class?Color:
????"""A?regular?class?that?represents?a?color."""
????r:?float
????g:?float
????b:?float
????alpha:?float
????def?__iter__(self):
????????yield?from?dataclasses.astuple(self)
性能比較
僅比較功能是不夠的,namedtuple 和數(shù)據(jù)類在性能上也有所不同。數(shù)據(jù)類基于純 Python 實(shí)現(xiàn) dict。這使得它們在訪問字段時更快。另一方面,namedtuples 只是常規(guī)的擴(kuò)展 tuple。這意味著它們的實(shí)現(xiàn)基于更快的 C 代碼并具有較小的內(nèi)存占用量。
為了證明這一點(diǎn),請考慮在 Python 3.8.5 上進(jìn)行此實(shí)驗(yàn)。
In?[6]:?import?sys
In?[7]:?ColorTuple?=?namedtuple("Color",?"r?g?b?alpha")
In?[8]:?@dataclass
???...:?class?ColorClass:
???...:?????"""A?regular?class?that?represents?a?color."""
???...:?????r:?float
???...:?????g:?float
???...:?????b:?float
???...:?????alpha:?float
???...:
In?[9]:?color_tup?=?ColorTuple(r=50,?g=205,?b=50,?alpha=1.0)
In?[10]:?color_cls?=?ColorClass(r=50,?g=205,?b=50,?alpha=1.0)
In?[11]:?%timeit?color_tup.r
36.8?ns?±?0.109?ns?per?loop?(mean?±?std.?dev.?of?7?runs,?10000000?loops?each)
In?[12]:?%timeit?color_cls.r
38.4?ns?±?0.112?ns?per?loop?(mean?±?std.?dev.?of?7?runs,?10000000?loops?each)
In?[15]:?sys.getsizeof(color_tup)
Out[15]:?72
In?[16]:?sys.getsizeof(color_cls)?+?sys.getsizeof(vars(color_cls))
Out[16]:?152
如上,數(shù)據(jù)類在中訪問字段的速度稍快一些,但是它們比 nametuple 占用更多的內(nèi)存空間。
如何將類型提示添加到 namedtuple
數(shù)據(jù)類默認(rèn)使用類型提示。我們也可以將它們放在 namedtuples 上。通過導(dǎo)入 Namedtuple 注釋類型并從中繼承,我們可以對 Color 元組進(jìn)行注釋。
from?typing?import?NamedTuple
...
class?Color(NamedTuple):
????"""A?namedtuple?that?represents?a?color."""
????r:?float
????g:?float
????b:?float
????alpha:?float
另一個可能未引起注意的細(xì)節(jié)是,這種方式還允許我們使用 docstring。如果輸入,help(Color)我們將能夠看到它們。
Help?on?class?Color?in?module?__main__:
class?Color(builtins.tuple)
?|??Color(r:?float,?g:?float,?b:?float,?alpha:?Union[float,?NoneType])
?|
?|??A?namedtuple?that?represents?a?color.
?|
?|??Method?resolution?order:
?|??????Color
?|??????builtins.tuple
?|??????builtins.object
?|
?|??Methods?defined?here:
?|
?|??__getnewargs__(self)
?|??????Return?self?as?a?plain?tuple.??Used?by?copy?and?pickle.
?|
?|??__repr__(self)
?|??????Return?a?nicely?formatted?representation?string
?|
?|??_asdict(self)
?|??????Return?a?new?dict?which?maps?field?names?to?their?values.
如何將可選的默認(rèn)值添加到 namedtuple
在上一節(jié)中,我們了解了數(shù)據(jù)類可以具有可選值。另外,我提到要模仿上的相同行為,namedtuple需要進(jìn)行一些技巧修改操作。事實(shí)證明,我們可以使用繼承,如下例所示。
from?collections?import?namedtuple
class?Color(namedtuple("Color",?"r?g?b?alpha")):
????__slots__?=?()
????def?__new__(cls,?r,?g,?b,?alpha=None):
????????return?super().__new__(cls,?r,?g,?b,?alpha)
>>>?c?=?Color(r=0,?g=0,?b=0)
>>>?c
Color(r=0,?g=0,?b=0,?alpha=None)
結(jié)論
元組是一個非常強(qiáng)大的數(shù)據(jù)結(jié)構(gòu)。它們使我們的代碼更清潔,更可靠。盡管與新的數(shù)據(jù)類競爭激烈,但他們?nèi)杂写罅康膱鼍翱捎?。在本教程中,我們學(xué)習(xí)了使用namedtuples的幾種方法,希望你可以使用它們。
參考資料
這里: https://docs.python.org/3/library/collections.html#collections.somenamedtuple._asdict
[2]文檔: https://docs.python.org/3/library/operator.html#operator.attrgetter
[3]PEP: https://www.python.org/dev/peps/pep-0557/#abstract

近期熱門文章推薦:
為什么說 Python 內(nèi)置函數(shù)并不是萬能的?

