有些類別的實例,是用來裝載資料,對於這樣的類別,我們稱為資料類別。表面上看來,這些類別只有一些欄位,只需要定義__init__方法,其他什麼都沒有,然而隨著需求增加,或許要定義物件描述(__str__、__repr__),可能要比較相等性或大小(__eq__、__lt__等),也許必須置入集合(hashable,也就是不可變動物件且具有__hash__)。

自行實作這類協定的問題,不單只是無趣、繁瑣的問題,若類別的欄位變更,相關的協定也須逐一檢視,做出相對應的修改,就維護而言,是個不小的負擔。

有欄位名稱的tuple

Python創建者Guido van Rossum曾經寫道:「避免過度設計資料結構。tuple比物件好(試試namedtuple)。簡單的欄位會比Getter/Setter函式好。」如果需要具有欄位、物件描述、hashable,我們可以使用collections模組的namedtuple,有些開發者會用它來權充資料類別,到了Python 3.6以後,還可以使用typing.NamedTuple,在定義時更為方便,而且可以結合型態提示,例如,定義具有欄位預設值的Point類別:

class Point(NamedTuple):
x: int = 0
y: int = 0

Point實例會根據欄位來定義__init__、__str__、__repr__,看來很方便,其實,指定的欄位也會用來實作__eq__、__lt__等方法,只不過在相等或大小比較時,Point實例本質上還是tuple,只是被額外賦予名稱,如果有個tuple具有相同的元素,或者是兩個namedtuple()傳回的類別,建構出的實例具有相同的元素,相互比較時,就會被判定為True,也就是說,namedtuple實例在比較時,並不在意型態。

除此之外,namedtuple也會具有__len__實作,實現了iterable協定,當然,實例的欄位值不可變動,因此別濫用namedtuple,如果需要tuple的特性,並且元素被額外賦予名稱時,才使用namedtuple。

Python 3.7的dataclass

Python 3.7新增了dataclasses模組,我們可以透過@dataclass裝飾器、field函式等來定義資料類別,Python之父曾經談到,其存在之目的,是為了補足Python標準程式庫在資料類別定義上的不足,能結合型態提示語法更方便地定義資料類別。例如,同樣是定義具有欄位預設值的Point類別:

@dataclass
class Point:
x: int = 0
y: int = 0

Point類別的實例,會根據欄位來定義__init__、__str__、__repr__、__eq__、__lt__等方法,而且,可以變動欄位值。因此,就這點而言,比使用namedtuple方便,然而,別忘了可變動物件是unhashable,因此,@dataclass裝飾的類別,不會有__hash__實作,若真的需要__hash__,可以設定@dataclass(frozen=True),令資料類別為不可變動,就能產生__hash__方法。

「嗯?不可變動?這樣不是跟tuple很像了?」如果欄位需要可以變動,又真的想有__hash__方法,可以設定@dataclass(unsafe_hash=True),強制實作加入__hash__,然而參數名稱上也表明了,這是個不安全的做法,畢竟這意謂著物件的hash值可能會改變(雖然有__hash__,然而不符合hashable的定義),在一些場合中會發生問題,例如,被加入set中的兩個Point實例,被誤變動後代表相同的點,這就違反了集合中的物件必不相同的要求了。

Python之父也說過,dataclasses的存在,是為了避免有人濫用namedtuple。如果需要的是資料類別,就用dataclasses來定義,就@dataclass裝飾的Point來說,若不是Point實例,相等比較時就會是False,也就是資料類別在相等比較時會在意型態。

為什麼不是attrs?

dataclasses是在Python 3.7實現,而更早版本的Python只能視情況使用namedtuple,或自行實作資料類別嗎?當然,社群中早有第三方程式庫作為解決的方案,其中,最有名的就是attrs,也曾經是呼聲最高,最有可能進入標準程式庫的方案,而在規範資料類別的PEP557中,還有個〈Why not just use attrs?〉特別說明一下,為什麼最後沒有直接採用attrs。

實際上,在dataclasses實作的REPO當中,也有個〈why not just attrs?〉的ISSUE,質疑了標準程式庫為什麼不直接採用attrs。Python之父的回應中提到:雖然attrs確實有許多很棒的想法,dataclasses有許多實現,確實也是吸收這些想法而來,然而,他也直言不諱地說:「Incorporating attrs is a bad idea」。

Python之父的回應提及,透過@dataclass來定義時語法比較簡單,而且,可以直接結合Python既有的語法,像是結合型態提示語法,以方才提到的x: int為例,當時相對應的attrs,定義是x = attr.ib(validator=attr.validators.instance_of(int)),看來就複雜許多,不過,新版本的attrs整合了型態提示而予以簡化了:

@attr.s
class Point:
x = attr.ib(type=int)
y: int = attr.ib()

其實,attrs在這之前,本來打算使用y: attr.ib(default=42)的寫法,不過,Python之父說,這誤用了PEP526,因為冒號右邊應該是個型態,attrs也就採納了Python之父的建議而予以修正了。這件事其實反映了第三方程式庫的彈性,然而,也道出了為什麼不直接整合attrs的另一個理由,那就是:程式庫的穩定性。

不只Python之父,社群中也有其他人提及:進入標準程式庫的方案必須是穩定的,也有人提出透過pip來控制版本,然而,Python之父直言:「the pip way is certainly not reasonable」,畢竟pip是用來安裝、升級第三方程式庫的方案,跟標準程式庫的升級是兩回事。

另一方面,attrs進入標準程式庫,意謂著它必須配合Python的釋出週期,attrs自身的官方文件後來也提及:未進入標準程式庫的理由之一,是為了不阻礙attrs的未來發展

視需求選擇方案

如果需要定義資料類別,Python 3.7以後,當然是優先採用dataclasses,畢竟是內建方案;attrs未進入標準程式庫,也不是什麼大的損失,畢竟dataclasses並未完全涵蓋attrs的全部功能,只是個相對簡單的方案,有進階需求,還是可以使用attrs;另一方面,attrs可以用於Python 3.6以前甚至是Python 2.7的版本,若考量相容性時,是個重要的選擇。

如果需要的是資料類別,別使用namedtuple,namedtuple就真的只是tuple,只是個簡單的資料結構,具有簡單的欄位,當需要的真的是tuple時,就會用得很開心,像是可以簡單地將namedtuple的欄位拆解給變數,這點dataclasses就做不到了。

總之,namedtuple、dataclasses與attrs各有其應用場合,清楚認識各自因特性是必要的,如此才能釐清自身需求下,應該採用哪個方案!

專欄作者

熱門新聞

Advertisement