Python字典(dict)使用技巧


字典dict是Python中使用頻率非常高的數據結構,關於它的使用,也有許多的小技巧,掌握這些小技巧會讓你高效地的使用dict,也會讓你的代碼更簡潔.

1.默認值

假設name_for_userid存放的是name和id的映射關系:

name_for_userid = {
    1: '張三',
    2: '李四',
    3: '王五',
}

獲取name_for_userid中的某一個id的name,最簡單的方式:

name_for_userid[1]
'張三'

這種方式雖然簡單,但有一個不便之處就是,如果獲取一個不存在於name_for_userid中的值程序會出現異常:

name_for_userid[4]
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
<ipython-input-138-66c7bf5cd53a> in <module>()
----> 1 name_for_userid[4]

KeyError: 4

很多時候我們不希望程序出現這種異常,為了避免這種情況,可以在獲取name之前先判斷該id是否已經存在name_for_userid中:

if 4 in name_for_userid:
    print(name_for_userid[4])
else:
    print("Not Found")
"Not Found"

這種寫法雖然可行,但是效率不高,因為獲取某一個id對應的name最多需要查詢兩次:第一次是先判斷該id是否存在name_for_userid中,若存在則第二次從name_for_userid中取出name值。還有一種寫法:

try:
    print(name_for_userid[4])
except KeyError:
    print("Not Found")
"Not Found"

這種寫法代碼有些冗余,不夠簡潔,所以,dict提供了get方法,這個方法的好處就是可以設值default值,對於那些不存在於dict中的key會返回default值作為它的value:

name_for_userid.get(4,"None")
"None"

2.排序

card = {'a': 4, 'c': 2, 'b': 3, 'd': 1}

用sorted對card排序,其實是用card的key進行排序,如下:

sorted(card.items())
[('a', 4), ('b', 3), ('c', 2), ('d', 1)]

有些時候我們需要對card的value進行排序,這個時候就可以使用sorted函數中的key這個參數,我們可以help一下這個函數的用法:

help(sorted)
Help on built-in function sorted in module builtins:
sorted(iterable, key
=None, reverse=False) Return a new list containing all items from the iterable in ascending order. A custom key function can be supplied to customise the sort order, and the reverse flag can be set to request the result in descending order.

所以我們可以自定義一個key函數sorted_by_value:

def sorted_by_value(item):
    return item[1]

sorted(card.items(),key=sorted_by_value)
[('d', 1), ('c', 2), ('b', 3), ('a', 4)]

其實,如果使用lambda匿名函數的話,代碼會更簡潔:

sorted(card.items(),key=lambda item:item[1])

 如果讓別人更輕松的理解你的意圖,你可以嘗試用下面的方法:

import operator
sorted(card.items(),key=operator.itemgetter(1))

需要注意的是,operator.itemgetter函數獲取的不是值,而是定義了一個函數,通過該函數作用到對象上才能獲取該對象上指定域的值。

其實,還是比較喜歡lambda表達式,因為它靈活,簡潔,比如,要對依據value的絕對值對dict排序,就可以這樣寫:

sorted(card.items(),key=lambda item:abs(item[1]))

3.switch...case

有句老話是這樣說的,Python過去現在以及以后都不會有switch...case語句。這是因為if…elif…else和dict都能實現switch...case語句,所以switch...case就沒有存在的必要了。

  • if...elif...else
if cond == 'cond_a':
    handle_a()
elif cond == 'cond_b':
    handle_b()
else:
    handle_default()

用if...elif...else來實現switch的功能,好處就是可讀性強,但是如果處理的條件比較多,這樣的寫法就有些啰嗦和冗余。所以,這個時候dict就又閃亮登場了。

  • dict
func_dict = {
    'cond_a': handle_a,
    'cond_b': handle_b
}

cond = 'cond_a'
func_dict[cond]()

相對於if...elif...else,dict就顯得清爽了許多,另外,如果想要實現default我們也可以使用dict的get()方法:

>>>func_dict.get(cond, handle_default)()

這樣即使cond不在func_dict中,程序也不會異常中止。

下面再舉一個例子,如下

>>>def dispatch_if(operator, x, y):
    if operator == 'add':
        return x + y
    elif operator == 'sub':
        return x - y
    elif operator == 'mul':
        return x * y
    elif operator == 'div':
        return x / y
>>>def dispatch_dict(operator, x, y):
    return {
        'add': lambda: x + y,
        'sub': lambda: x - y,
        'mul': lambda: x * y,
        'div': lambda: x / y,
    }.get(operator, lambda: None)()

如果想的再深入點的話,上面的例子就性能上來說不是最優的,因為,每次調用dispatch_dict函數的時候都會生成一個臨時的包含各種操作碼(加減乘除)的字典,最好的情況當然是這個字典只生成一次(常量),下次再調用此函數的時候,直接使用此字典就可以了,另外,python中的operator模塊已經實現了加減乘除如:operator.mul, operator.div ,完全可以替代lambda表達式。這里只是舉個例子以更好的明白if…elif…else和dict的異同。

4.合並dict的幾種方法

有的時候需要用一個dict去更新另一個dict,比如,用戶自定義的配置文件去覆蓋默認配置,等等。假設有下面兩個dict:

>>> xs = {'a': 1, 'b': 2}
>>> ys = {'b': 3, 'c': 4}

最常用的就是用dict內置的update()方法:

>>> zs = {}
>>> zs.update(xs)
>>> zs.update(ys)
>>> zs
{'a': 1, 'b': 3, 'c': 4}

還有一種方法就是使用內置的dict():

>>> zs = {**xs, **ys}
{'a': 1, 'b': 3, 'c': 4}

5.美觀打印

當打印一些調試信息的時候,美觀的打印輸出有的時候能讓人很直觀的看出關鍵信息,提高調試的效率。

最朴素的打印:

>>> mapping = {'a': 23, 'b': 42, 'c': 0xc0ffee}
>>> str(mapping)
"{'c': 12648430, 'a': 23, 'b': 42}"

借助內置模塊json可以實現更加直觀的表現形式:

>>> import json
>>> json.dumps(mapping, indent=4, sort_keys=True)
{
    "a": 23,
    "b": 42,
    "c": 12648430
}

但是這種方法有一定的限制:

>>> mapping['d'] = {1, 2, 3}
>>> json.dumps(mapping)
TypeError: "set([1, 2, 3]) is not JSON serializable"

>>> json.dumps({all: 'yup'})
TypeError: "keys must be a string"

所以,還可以使用下面的方式:

>>> import pprint
>>> pprint.pprint(mapping)
{'a': 23, 'b': 42, 'c': 12648430, 'd': set([1, 2, 3])}

6.彩蛋

 最后介紹幾個關於dict比較有意思的東西 .

>>>print("keys:",list({True: 'yes', 1: 'no', 1.0: 'maybe'}.keys()))
>>>print("values:",list({True: 'yes', 1: 'no', 1.0: 'maybe'}.values()))

上面的代碼會打印出什么結果呢,一開始我認為是下面的輸出:

keys: [True,1,1.0]
values: ['yes','no','maybe']

真是too young too simple,其實結果應是:

keys: [True]
values: ['maybe']

why?再仔細想想的化,既然key是True,那value為什么不是yes而是maybe,再者,既然value是maybe,那key值為什么不是1.0而是True呢?你們的疑問是不是和我一樣?

其實,當Python在處理dict表達式的時候,它會先根據dict類創建出dict的實例,然后按照item(key:value)在dict表達式出現的順序依次賦值,如下:

>>>xs = dict()
>>>xs[True] = 'yes'
>>>xs[1] = 'no'
>>>xs[1.0] = 'maybe'

更奇怪的是,dict會用下面的表達式認為他的key值都是相等的:

>>>True == 1 == 1.0
True

看到1==1.0你也許不會感到奇怪,但是為什么會有True==1?當時看到這里我也有點懵逼的感覺.

翻閱了一下Python的官方文檔,在這一章節The standard type hierarchy中發現下面這段話:

“The Boolean type is a subtype of the integer type, and Boolean values behave like the values 0 and 1, respectively, in almost all contexts, the exception being that when converted to a string, the strings ‘False’ or ‘True’ are returned, respectively.”

意思就是說,Boolean是Int的子類,就是0和1.那么我們可以驗證一下:

>>>["No","Yes"][True]

結果果然是Yes,

>>>["No","Yes"][False]

結果也果然是No.但是為了清晰起見,不建議這樣用.

看到這里你可能會問,這和dict有毛關系.接着往下看:

就Python而言,True,1和1.0都表示相同的字典key值,當解釋器在執行dict表達式的時候,它會把后面的同一個key(True)的value值覆蓋掉這個key(True)之前的value值.所以就會有下面的結果:

>>>{True: 'yes', 1: 'no', 1.0: 'maybe'}
{True: 'maybe'}

但是key為什么沒有被最后的1.0覆蓋呢?其實道理也很簡單,既然是一樣的key,為什么還要多此一舉再用多余的時間去更新"相同的"key呢?這是出於CPython解釋器性能的考慮.

>>>ys = {1.0: 'no'}
>>>ys[True] = 'yes'
>>>ys
輸出:{1.0: 'yes'}

 根據我們現在所了解到的,從表面上看是當key值相同時才會覆蓋掉已有的value值,但是,事實證明,這不僅僅和判定相等的__eq__有關系.

Python的字典是通過哈希表實現的,也就是說,它通過把key映射到表中一個位置來訪問記錄,以加快查找的速度。這個映射函數叫做哈希函數,存放記錄的數組叫做哈希表。但是沒有十分完美的哈希函數計算出來的存儲位置都是不一樣的,所以就會存在哈希沖突的情況,也就是不同的key計算出來的存儲位置是同一個地址.下面來看看究竟是什么導致了更新value值.

  • 先定義如下的一個類:
>>>class AlwaysEquals:
    def __eq__(self, other):
        return True
    def __hash__(self):
        return id(self)
AlwaysEquals有兩個方法:__eq____hash__.
首先,由於__eq__返回True,所以下面的條件表達式都是True:
>>>AlwaysEquals() == AlwaysEquals()
>>>AlwaysEquals() == 42
>>>AlwaysEquals() == 'waaat?'

其次,由於__hash__返回該對象的id值,也就是內存地址,所以有:

>>>objects = [AlwaysEquals(),AlwaysEquals(),AlwaysEquals()]
>>>[hash(obj) for obj in objects]
[140388524604608, 140388524604664, 140388524604720]

所以,綜上,當值相等而hash值不相等時候,是否會存在value值覆蓋:

>>>{AlwaysEquals(): 'yes', AlwaysEquals(): 'no'}

結果是:

{<__main__.AlwaysEquals at 0x7faec023bbe0>: 'yes',<__main__.AlwaysEquals at 0x7faec023bac8>: 'no'}
  • 重新定義一個類SameHash:
>>>class SameHash:
    def __hash__(self):
        return 1
>>>a = SameHash()
>>>b = SameHash()

由於__hash__返回1,所以有:

>>> a == b
False
>>> hash(a), hash(b)
(1, 1)

當值不相等而hash值相等的時候,是否會存在value值覆蓋:

>>> {a: 'a', b: 'b'}
{ <SameHash instance at 0x7f7159020cb0>: 'a',<SameHash instance at 0x7f7159020cf8>: 'b' }

綜上這兩種情況,value值都不會被更新。

{True: 'yes', 1: 'no', 1.0: 'maybe'}  會出現更新key所對應的value值,是因為:

>>> True == 1 == 1.0
True
>>> (hash(True), hash(1), hash(1.0))
(1, 1, 1)

他們同時滿足條件表達式相等且hash值也相等。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM