字典的平行迭代

由於字典是無序的,鮮少需要進行平行迭代,不過在實際的應用中我們還是能夠遇到這種問題,請看下面的例子。

我們有兩個字典,分別是全班學生在去年度跟今年度的成績:

# 請注意,雖然這兩個字典的鍵值對是依相同順序設定的,但實際上他們都是無序的

last_year = {
    'Bill': 90,
    'Cynthia': 85,
    'dokelung': 60,
}

this_year = {
    'Bill': 100,
    'Cynthia': 100,
    'dokelung': 95,
}

從去年到今年,學生們的成績都是進步的,但我們想要知道每個學生的進步幅度有多大,這時就出現了平行迭代的需求,我們也想要進行類似 zip 的動作,能夠將同一個學生去年跟今年的成績組合在一次迴圈中進行相除的動作以算出進步的幅度。

下面這種作法我認為是合理的:

for name in this_year:
    last_score = last_year[name]
    this_score = this_year[name]
    percent = ((this_score-last_score)/last_score) * 100
    print('{}={:.3}%'.format(name, percent))

結果:

dokelung=58.3%
Bill=11.1%
Cynthia=17.6%

這個寫法雖然違背了之前講述過的原則:別在迴圈中取值,但是我認為以這個問題而言,不需要太過強求,上面的做法已經相當妥當了.

但如果硬是要做到在 for 述句中將迴圈變數設定好,那我們可能得寫一個額外的產生器函式:

def zipdic(*dics):
    for key in dics[0]:
        yield (key,) + tuple(dic[key] for dic in dics)

然後就可以維持我們的原則來使用了:

for name, last_score, this_score in zipdic(last_year, this_year):
    percent = ((this_score-last_score)/last_score) * 100
    print('{}={:.3}%'.format(name, percent))

zipdic 是個產生器函式,同時也是個迭代器,他能夠接受多個字典作為引數,並藉由迭代第一個字典的鍵,產生出該鍵在所有字典中對應值組成的 元組 (包含該鍵)。

關於產生器、迭代器和變數拆解等議題可以參考本書的其他章節。

最後,關於一般的做法跟本節介紹的手段孰優孰劣的問題,就見仁見智了。你可以覺得產生器函式的方法太過複雜且沒有能夠少寫多少代碼;你也可以認為這個產生器函式能夠在相同或相似的問題上不斷地派上用場且遵循我們的基本原則。

平行迭代含有不同鍵的字典

跟清單的平行迭代一樣,我們也會碰到多個字典的長度不同,或是其鍵不相等的狀況,我們考慮以下情境:

last_year = {
    'Bill': 90,
    'Cynthia': 85,
    'dokelung': 60,
    'Mike': 55,
}

this_year = {
    'Bill': 100,
    'Cynthia': 100,
    'dokelung': 95,
    'Mary': 99,
}

去年班上有位叫做 Mike 的同學,但他今年轉學走了,並且轉進了一位名叫 Mary 的同學。因為這兩位同學的成績資料不齊全,我們無法計算出他們的進步幅度,於是在這個情境下我們想要略過這兩位同學只報出兩年都在同一個班級的學生進步幅度。

要做到這一點並不困難,我們適度地修改一下 zipdic 函式:

def zipdic(*dics):
    for key in set(dics[0]).intersection(*dics[1:]):
        yield (key,) + tuple(dic[key] for dic in dics)

這邊我們將字典的鍵分別轉為集合並且取其交集,我們只處理字典的共有鍵。

更完美的 zipdic

上面的 zipdic 在大部分的狀況下都很好用了,但是我們可以設法讓他更完美。

首先,該函式並不十分強健穩固,若是 zipdic 沒有傳入參數的話:

for name, last_score, this_score in zipdic(): # 假設出現這種極端的狀況
    # do something ...

會引發錯誤:

IndexError: tuple index out of range

要避免這個問題,我們必須做的更小心,讓我們加強原本的 zipdic

def zipdic(*dics):
    if len(dics)==0: # 補上一個檢查,若沒有參數傳入則停止迭代
        return
    for key in set(dics[0]).intersection(*dics[1:]):
        yield (key,) + tuple(dic[key] for dic in dics)

其二,原本的 zip 函式會將多個清單組合成一個一個的元組:

>>> lst1 = [1, 2, 3]
>>> lst2 = ['a', 'b', 'c']
>>> list(zip(lst1, lst2))
[(1, 'a'), (2, 'b'), (3, 'c')]

等於是將序列組合成新的序列;那我們的 zipdic 也應該是將映射形態的字典組合為映射才是,所以讓我們修改一下:

def zipdic(*dics):
    if len(dics)==0: # 補上一個檢查,若沒有參數傳入則停止迭代
        return
    for key in set(dics[0]).intersection(*dics[1:]):
        yield key, tuple(dic[key] for dic in dics)

這兩者的改變只有一點點,但意義上差很多,原本我們將鍵與所有對應的值拼組起來成為元組,而現在我們先讓對應的值先組成一個子元組,再與鍵形成一個最終的元組,這樣做讓我們展現出了鍵值對應這種 映射關係

但要注意,現在 zipdic 的使用方式跟以前不同了,我們要多進行一個層級的拆解 (嵌套拆解):

for name, (last_score, this_score) in zipdic(last_year, this_year):
    # do something ...

不過這是值得的,現在 zipdic 已經能夠產生映射型態的東西了 (而第一個版本的不行):

# 第一個版本的 zipdic,其返回值無法轉為字典
>>> dict(zipdic(last_year, this_year))
ValueError: dictionary update sequence element #0 has length 3; 2 is required
# 第二個版本的 zipdic,能夠順利轉化為字典
>>> dict(zipdic(last_year, this_year))
{'Bill': (90, 100), 'Cynthia': (85, 100), 'dokelung': (60, 95)}

results matching ""

    No results matching ""