iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 4
1
Data Technology

30天python雜談系列 第 4

collections雜談之二 ——— 看似雞肋的tuple

python collections雜談之二

在介紹下一個好用的數據結構namedtuple前,我先來談一下tuple和list的差別,以前我在初學python的時候,對於元素組我都是用list來包裝,因為tuple只要一初始化就不可以再做更動了,感覺要新增或是修改數據什麼的就非常的不方便,所以tuple對當時的我來說算是一個雞肋之物,但過了一段時間接觸多了,開始了解了tuple的好處,首先在前一兩天的動態型別雜談有提到所謂的immutable object和mutable object,因為內建的類型屬於mutable object就只有list以及dict,那所以我就可以說tuple算是一個'不可變'的list,從'不可變'的list來發想,究竟tuple可以有什麼用處呢:

  1. 因為不可變,當然就用在不會變動的資料上阿!
  2. 因為不可變,當然就用在不能給人變動的資料上阿!

這兩種對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功能。

好了言歸正傳,來談談這系列的第三個數據結構吧!

3. namedtuple:

在使用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的心得。


上一篇
collections雜談之一 ——— dict的key值存不存在乾我屁事
下一篇
collection雜談之三———list的限制與解決方案
系列文
30天python雜談30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
davidhcefx
iT邦新手 5 級 ‧ 2021-02-20 05:32:07

很棒的介紹!原版 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

我要留言

立即登入留言