在介紹下一個好用的數據結構namedtuple前,我先來談一下tuple和list的差別,以前我在初學python的時候,對於元素組我都是用list來包裝,因為tuple只要一初始化就不可以再做更動了,感覺要新增或是修改數據什麼的就非常的不方便,所以tuple對當時的我來說算是一個雞肋之物,但過了一段時間接觸多了,開始了解了tuple的好處,首先在前一兩天的動態型別雜談有提到所謂的immutable object和mutable object,因為內建的類型屬於mutable object就只有list以及dict,那所以我就可以說tuple算是一個'不可變'的list,從'不可變'的list來發想,究竟tuple可以有什麼用處呢:
這兩種對tuple功能的發想都是對的,因為tuple相對於list來說是不可變的,所以其內部結構較簡單,操作也比較快,如果在程式中想要設置一個常量元素組,他只用來訪問或用迴圈遍歷,而不會用來更改,那tuple當然是較好的選擇,而tuple也為數據提供了安全性,若你想要保護程式中的某些數據不被其他專案成員用程式更改,那tuple也是比list來的更適合,而另外函數間的多參數傳遞其實也是用tuple來實作的。
科科,當初初學python時,python最吸引我的一個小功能竟然也是因為tuple才能達成,我竟然後來才知道這件事,阿!當初從tuple中受益這麼久,竟如此身在福中不知福。
一定有很多人知道在python裡有一個很美妙的指令可以達成兩變數間的swap,就是"x,y=y,x",這其中的原理究竟是什麼呢?事實上等號右式的"y,x"已經構成了一個新的tuple對象,所以已經跟y,x兩個變數本身沒有什麼直接相關了,然後再藉由tuple的parallel assignment,讓"x=(y,x)[0]"以及"y=(y,x)[1]",用一行指令很簡潔的實現了swap功能。
好了言歸正傳,來談談這系列的第三個數據結構吧!
在使用tuple結構的時候,通常會有一個可讀性與維護性上的困擾,如果我們要訪問tuple裡的的其中一個值,取他的index值就很容易就能達成,但是通常這個index值對於功能上並沒有任何意義,他只指名某個數據存放在tuple的第幾個位置裡。
以可讀性來說,我沒辦法以index值來得知存放在此的數據代表的意義,以維護性來說,假設我要在程式碼的某個tuple裡新插入某些元素,但這導致位於插入元素之後的其他元素的index值都會被改變,若是程式的其他地方有去訪問到這些index值被改變的元素,我都必須一一更改我訪問的位置,這是一件相當煩瑣的事。
有鑑於此,collections也提供了一個好用的數據結構namedtuple,讓我們可以利用名稱來代替index值,不僅改善可讀性,也可以減少元素與位置過強的耦合性,以下來稍微示範一下namedtuple的使用方法:
In python3 shell:
>>> from collections import namedtuple
>>> Identity = namedtuple('Identity', ['first_name','last_name','birthday'])
>>> identity = Identity('Sam','Lee','4/2')
>>> identity
Identity(first_name='Sam', last_name='Lee', birthday='4/2')
>>> identity.first_name
'Sam'
>>> identity.last_name
'Lee'
>>> identity.birthday
'4/2'
>>> identity[0]
'Sam'
>>> identity[1]
'Lee'
>>> identity[2]
'4/2'
當元素組的訪問可以依賴於名稱時,就近似於dict的功能了,然而不一樣的是,namedtuple是一個immutable object,但也因此他所需要的空間比字典還要來的少,但字典對於key值的搜尋速度比namedtuple快(理想上python的dict在搜尋key值的時間複雜度是O(1),而namedtuple基本上還是tuple結構,所以其時間複雜度為O(n)),直覺上會覺得若是較重視空間效率的程式會偏好使用namedtuple,但速度和dict實在是差太多了,就我個人的經驗覺得可以取代dict的狀況還是少的,更何況他還不能直接更改value或是新增key值,但對於優化tuple本身的可讀性卻是極為推薦的。
順帶提一下,namedtuple的初始化是很方便的,他支援許多的型式,除了上式的string list,以下的方法也是同樣意思:
Identity = namedtuple('Identity', 'first_name,last_name,birthday')
Identity = namedtuple('Identity', 'first_name last_name birthday')
然而雖不能直接在原有的對象上更改或新增數據,因其immutable的特性,但namedtuple仍然提供一些方便的方法,這些方法是利用直接重造一個新的tuple的方式來更改數據:
In python3 shell:
>>> from collections import namedtuple
>>> Identity = namedtuple('Identity', ['first_name','last_name','birthday'])
>>> identity = Identity('Sam','Lee','4/2')
>>> identity._replace(birthday='4/3')
Identity(first_name='Sam', last_name='Lee', birthday='4/3')
因為這個_replace方法是接受keyword參數的,因此在python cookbook有提及這個方法可以讓我們選擇性的只指定某些欄位的值,只要一開始有建立一個具有default值的namedtuple就行了(以下部份引用cookbook的code):
from collections import namedtuple
Stock = namedtuple('Stock',['name','shares','price'])
default_stock = Stock('',0,0.0)
def dict_to_stock(s):
return default_stock._replace(**s)
print(dict_to_stock({'shares':5}))
print(dict_to_stock({'shares':5,'name':'LALA'}))
好拉,今天就到這裡結束,明天再繼續整理一些有關collections的心得。
很棒的介紹!原版 tuple
的痛點也有點到。
不過有一段,我覺得我有必要幫 namedtuple
來平反一下XD
但字典對於key值的搜尋速度比namedtuple快(理想上python的dict在搜尋key值的時間複雜度是O(1),而namedtuple基本上還是tuple結構,所以其時間複雜度為O(n)),直覺上會覺得若是較重視空間效率的程式會偏好使用namedtuple,但速度和dict實在是差太多了
經過實測,namedtuple
在使用 dot operator 存取元素時,確實是 O(1) 複雜度。
因此,並沒有使用 dict
就保證比 namedtuple
快這回事;
應該說各有優缺,前者擅長處理動態加入的資料,後者則擅長固定格式的資料存取。
測試程式碼:
import random
from collections import namedtuple
from timeit import timeit
Result = namedtuple('Result', 'dict, namedtuple')
def test(size: int) -> Result:
d = dict({f'x{i}': i ** 2 for i in range(size)})
T = namedtuple('T', d.keys())
t = T(**d)
keys = list(d.keys())
seq = [random.choice(keys) for i in range(10000)]
namespace = {'d': d, 't': t, 'seq': seq}
num = 1000
return Result(
dict=timeit('[d.get(s) for s in seq]', globals=namespace, number=num),
namedtuple=timeit('[t.__getattribute__(s) for s in seq]', globals=namespace, number=num)
)
print('{:<4}{:<20}{:<20}'.format('n', 'dict', 'namedtuple'))
for n in [4, 8, 16, 32, 64, 128, 250]:
res = test(n)
print('{:<4}{:<20}{:<20}'.format(n, res.dict, res.namedtuple))
測試結果:
n dict namedtuple
4 0.9689492999896174 1.5122128999937559
8 0.9820901000057347 1.491594699997222
16 1.0222421000071336 1.49489370000083
32 1.0260545000055572 1.491869699995732
64 1.0501393000013195 1.5114477000024635
128 1.028823899992858 1.5206703000003472
250 1.0596862999955192 1.5842664999945555