python字典 更新


python字典

字典即映射

類似於java中的map集合,KV類型的數據結構。以下為書中解釋

字典 與列表類似,但是更加通用。 在列表中,索引必須是整數;但在字典中,它們可以是(幾乎)任何類型。

字典包含了一個索引的集合,被稱為 鍵(keys) ,和一個值(values)的集合。 一個鍵對應一個值。這種一一對應的關聯被稱為 鍵值對(key-value pair) , 有時也被稱為 項(item)

在數學語言中,字典表示的是從鍵到值的 映射,所以你也可以說每一個鍵 “映射到” 一個值。 舉個例子,我們接下來創建一個字典,將英語單詞映射至西班牙語單詞,因此鍵和值都是字符串。

dict函數生成一個不含任何項的新字典。 由於 dict 是內建函數名,你應該避免使用它來命名變量。

>>> eng2sp = dict()
>>> eng2sp
{}

花括號 {} 表示一個空字典。你可以使用方括號向字典中增加項:

>>> eng2sp['one'] = 'uno'

這行代碼創建一個新項,將鍵 'one' 映射至值 'uno'。 如果我們再次打印該字典,會看到一個以冒號分隔的鍵值對:

eng2sp
{'one': 'uno'}

輸出的格式同樣也是輸入的格式。 例如,你可以像這樣創建一個包含三個項的字典:

>>> eng2sp = {'one': 'uno', 'two': 'dos', 'three': 'tres'}

但是,如果你打印 eng2sp ,結果可能會讓你感到意外:

>>> eng2sp
{'one': 'uno', 'three': 'tres', 'two': 'dos'}

鍵-值對的順序和原來不同。 同樣的例子在你的電腦上可能有不同的結果。通常來說,字典中項的順序是不可預知的。

但這沒有關系,因為字典的元素不使用整數索引來索引,而是用鍵來查找對應的值:

>>> eng2sp['two']
'dos'

'two' 總是映射到值 'dos' ,因此項的順序沒有關系。

如果鍵不存在字典中,會拋出一個異常:

>>> eng2sp['four']
KeyError: 'four'

len函數也適用於字典;它返回鍵值對的個數:

>>> len(eng2sp)
3

in操作符也適用於字典;它可以用來檢驗字典中是否存在某個 (僅僅有這個值還不夠)。

>>> 'one' in eng2sp
True
>>> 'uno' in eng2sp
False

想要知道字典中是否存在某個值,你可以使用 values 方法,它返回值的集合,然后你可以使用 in 操作符來驗證:

>>> vals = eng2sp.values()
>>> 'uno' in vals
True

in操作符對列表和字典采用不同的算法。 對於列表,它按順序依次查找目標,如搜索一節所示。 隨着列表的增長,搜索時間成正比增長。

對於字典,Python使用一種叫做 哈希表(hashtable) 的算法, 這種算法具備一種了不起的特性: 無論字典中有多少項,in 運算符搜索所需的時間都是一樣的。

字典作為計數器集合

假設給你一個字符串,你想計算每個字母出現的次數。 有多種方法可以使用:

  1. 你可以生成26個變量,每個對應一個字母表中的字母。然后你可以遍歷字符串,對於 每個字符,遞增相應的計數器,你可能會用到鏈式條件。
  2. 你可以生成具有26個元素的列表。然后你可以將每個字符轉化為一個數字(使用內建函數 ord ),使用這些數字作為列表的索引,並遞增適當的計數器。
  3. 你可以生成一個字典,將字符作為鍵,計數器作為相應的值。字母第一次出現時,你應該向字典中增加一項。 這之后,你應該遞增一個已有項的值。

每個方法都是為了做同一件事,但是各自的實現方法不同。

實現 是指執行某種計算的方法;有的實現更好。 例如,使用字典的實現有一個優勢,即我們不需要事先知道字符串中有幾種字母, 只要在出現新字母時分配空間就好了。

代碼可能是這樣的:

def histogram(s):
    d = dict()
    for c in s:
        if c not in d:
            d[c] = 1
        else:
            d[c] += 1
    return d

函數名叫 histogram (直方圖) ,是計數器(或是頻率)集合的統計術語。

函數的第一行生成一個空字典。for 循環遍歷該字符串。 每次循環,如果字符 c 不在字典中, 我們用鍵 c 和初始值 1 生成一個新項 (因為該字母出現了一次)。 如果 c 已經在字典中了,那么我們遞增 d[c]

下面是運行結果:

>>> h = histogram('brontosaurus')
>>> h
{'a': 1, 'b': 1, 'o': 2, 'n': 1, 's': 2, 'r': 2, 'u': 2, 't': 1}

histogram函數表明字母 'a''b' 出現了一次, 'o' 出現了兩次,等等。

字典類有一個 get 方法,接受一個鍵和一個默認值作為參數。 如果字典中存在該鍵,則返回對應值;否則返回傳入的默認值。例如:

>>> h = histogram('a')
>>> h
{'a': 1}
>>> h.get('a', 0)
1
>>> h.get('b', 0)
0

循環和字典

for 循環中使用字典會遍歷其所有的鍵。 例如,下面的 print_hist 會打印所有鍵與對應的值:

def print_hist(h):
    for c in h:
        print(c, h[c])

輸出類似:

>>> h = histogram('parrot')
>>> print_hist(h)
a 1
p 1
r 2
t 1
o 1

重申一遍,字典中的鍵是無序的。 如果要以確定的順序遍歷字典,你可以使用內建方法 sorted

>>> for key in sorted(h):
...     print(key, h[key])
a 1
o 1
p 1
r 2
t 1

逆向查找

給定一個字典 d 以及一個鍵 t ,很容易找到相應的值 v = d[k] 。 該運算被稱作 查找(lookup)

但是如果你想通過 v 找到 k 呢? 有兩個問題:第一,可能有不止一個的鍵其映射到值v。 你可能可以找到唯一一個,不然就得用 list 把所有的鍵包起來。 第二,沒有簡單的語法可以完成 逆向查找(reverse lookup);你必須搜索。

下面這個函數接受一個值並返回映射到該值的第一個鍵:

def reverse_lookup(d, v):
    for k in d:
        if d[k] == v:
            return k
    raise LookupError()

該函數是搜索模式的另一個例子,但是它使用了一個我們之前沒有見過的特性,raiseraise 語句 能觸發異常,這里它觸發了 ValueError,這是一個表示查找操作失敗的內建異常。

如果我們到達循環結尾,這意味着字典中不存在 v 這個值,所以我們觸發一個異常。

下面是一個成功逆向查找的例子:

>>> h = histogram('parrot')
>>> key = reverse_lookup(h, 2)
>>> key
'r'

和一個失敗的例子:

>>> key = reverse_lookup(h, 3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 5, in reverse_lookup
LookupError

你觸發的異常和 Python 觸發的產生效果一樣:都打印一條回溯和錯誤信息。

raise語句接受一個詳細的錯誤信息作為可選的實參。 例如:

>>> raise LookupError('value does not appear in the dictionary')
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
LookupError: value does not appear in the dictionary

逆向查找比正向查找慢得多; 如果你頻繁執行這個操作或是字典很大,程序性能會變差。

字典和列表

在字典中,列表可以作為值出現。 例如,如果你有一個從字母映射到頻率的字典, 而你想倒轉它; 也就是生成一個從頻率映射到字母的字典。 因為可能有些字母具有相同的頻率,所以在倒轉字典中的每個值應該是一個字母組成的列表。

下面是一個倒轉字典的函數:

def invert_dict(d):
    inverse = dict()
    for key in d:
        val = d[key]
        if val not in inverse:
            inverse[val] = [key]
        else:
            inverse[val].append(key)
    return inverse

每次循環,keyd 獲得一個鍵和相應的值 val 。 如果 val 不在 inverse 中,意味着我們之前沒有見過它, 因此我們生成一個新項並用一個 單元素集合(singleton) (只包含一個元素的列表)初始化它。 否則就意味着之前已經見過該值,因此將其對應的鍵添加至列表。

舉個例子:

>>> hist = histogram('parrot')
>>> hist
{'a': 1, 'p': 1, 'r': 2, 't': 1, 'o': 1}
>>> inverse = invert_dict(hist)
>>> inverse
{1: ['a', 'p', 't', 'o'], 2: ['r']}

圖11-1:狀態圖

圖11-1:狀態圖

圖11-1:狀態圖是關於 histinverse 的狀態圖。字典用標有類型dict的方框表示,方框中是鍵值對。如果值是整數、浮點數或字符串, 我就把它們畫在方框內部,但我通常把列表畫在方框外面,目的只是為了不讓圖表變復雜。

如本例所示,列表可以作為字典中的值,但是不能是鍵。 下面演示了這樣做的結果:

>>> t = [1, 2, 3]
>>> d = dict()
>>> d[t] = 'oops'
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
TypeError: list objects are unhashable

我之前提過,字典使用哈希表實現,這意味着鍵必須是 可哈希的(hashable)

哈希(hash) 函數接受一個值(任何類型)並返回一個整數。 字典使用被稱作哈希值的這些整數,來存儲和查找鍵值對。

如果鍵是不可變的,那么這種實現可以很好地工作。 但是如果鍵是可變的,如列表,那么就會發生糟糕的事情。 例如,當你生成一個鍵值對時,Python哈希該鍵並將其存儲在相應的位置。 如果你改變鍵然后再次哈希它,它將被存儲到另一個位置。 在那種情況下,對於相同的鍵,你可能有兩個值, 或者你可能無法找到一個鍵。 無論如何,字典都不會正確的工作。

這就是為什么鍵必須是可哈希的,以及為什么如列表這種可變類型不能作為鍵。 繞過這種限制最簡單的方法是使用元組, 我們將在下一章中介紹。

因為字典是可變的,因此它們不能作為鍵,但是 可以 用作值。

全局變量

在前面的例子中,known 是在函數的外部創建的, 因此它屬於被稱作 __main__ 的特殊幀。 因為 __main__ 中的變量可以被任何函數訪問,它們也被稱作 全局變量(global) 。 與函數結束時就會消失的局部變量不同,不同函數調用時全局變量一直都存在。

全局變量普遍用作 標記(flag); 也就是說明(標記)一個條件是否為真的布爾變量。 例如,一些程序使用一個被稱作 verbose 的標記來控制輸出的豐富程度:

verbose = True

def example1():
    if verbose:
        print('Running example1')

如果你試圖對一個全局變量重新賦值,結果可能出乎意料。 下面的例子本應該記錄函數是否已經被調用過了:

been_called = False

def example2():
    been_called = True         # 錯誤

但是如果你運行它,你會發現 been_called 的值並未發生改變。 問題在於 example2 生成了一個新的被稱作 been_called 的局部變量。 當函數結束的時候,該局部變量也消失了,並且對全局變量沒有影響。

要在函數內對全局變量重新賦值,你必須在使用之前 聲明(declare) 該全局變量:

been_called = False

def example2():
    global been_called
    been_called = True

global 語句 告訴編譯器,“在這個函數里,當我說 been_called 時,我指的是那個全局變量,別生成局部變量”。

下面是一個試圖更新全局變量的例子:

count = 0

def example3():
    count = count + 1          # 錯誤

一旦運行,你會發現:

UnboundLocalError: local variable 'count' referenced before assignment

Python默認 count 是局部變量,在這個假設下,你這是在未寫入任何東西前就試圖讀取。 解決方法還是聲明 count 是全局變量。

def example3():
    global count
    count += 1

如果全局變量是可變的,你可以不加聲明地修改它:

known = {0:0, 1:1}

def example4():
    known[2] = 1

因此你可以增加、刪除和替代全局列表或者字典的元素, 但是如果你想對變量重新賦值,你必須聲明它:

def example5():
    global known
    known = dict()

全局變量有時是很有用的,但如果你的程序中有很多全局變量,而且修改頻繁, 這樣會增加程序調試的難度。

調試

當你操作較大的數據集時,通過打印並手工檢查數據來調試很不方便。 下面是針對調試大數據集的一些建議:

縮小輸入:

如果可能,減小數據集合的大小。 例如,如果程序讀入一個文本文件,從前10行開始分析,或是找到更小的樣例。 你可以選擇編輯讀入的文件,或是(最好)修改程序使它只讀入前 n 行。

如果出錯了,你可以將 n 縮小為會導致該錯誤的最小值,然后在查找和解決錯誤的同時,逐步增加 n 的值。

檢查摘要和類型:

考慮打印數據的摘要,而不是打印並檢查全部數據集合: 例如,字典中項的數目或者數字列表的總和。

運行時錯誤的一個常見原因,是值的類型不正確。 為了調試此類錯誤,打印值的類型通常就足夠了。

編寫自檢代碼:

有時你可以寫代碼來自動檢查錯誤。 例如,如果你正在計算數字列表的平均數,你可以檢查其結果是不是大於列表中最大的元素,或者小於最小的元素。 這被稱 作“合理性檢查”,因為它能檢測出“不合理的”結果。

另一類檢查是比較兩個不同計算的結果,來看一下它們是否一致。這被稱作“一致性檢查”。

格式化輸出:

格式化調試輸出能夠更容易定位一個錯誤。 我們在調試一節中看過一個示例。pprint 模塊提供了一個 pprint 函數,它可以更可讀的格式顯示內建類型( pprint 代表 “pretty print”)。

重申一次,你花在搭建腳手架上的時間能減少你花在調試上的時間。


免責聲明!

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



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