Python中的映射類型詳解


# ------------------------------------泛映射類型------------------------------------
# collections.abc模塊中有Mapping和MutableMapping這兩個抽象基類,它們的作用事為dict和其他類似的類型定義形式接口
# 非抽象映射類型一般不會直接繼承這些抽象基類,它們會直接對dict或者是collections.UserDict進行擴展.這些抽象基類的主要作用事作為形式化的文檔,
# 它們定義了構建一個映射類型所需要的最基本的接口.然后它們還可以跟isinstance一起被用來判定某個數據是不是廣義上的映射類型:
# from collections.abc import Mapping, MutableMapping

# 標准庫里的所有映射類型都是利用dict來實現的,因此它們有個共同的限制,即只有<可散列的>數據類型才能用作這些映射里的鍵.
# 什么是可散列的數據類型?
# 如果一個對象是可散列的,那么在這個對象的生命周期中,它的散列值是不變的,而且這個對象需要實現__hash__()方法.
# 另外可散列對象還要有__eq__()方法,這樣才能跟其他鍵作比較.如果兩個可散列對象是相等的,那么它們的散列值一定是一樣的.

# 原子不可變數據類型(str,bytes和數值類型)都是可散列類型,frozenset也是可散列的,因為根據其定義,frozenset里只能容納可散列類型.
# 元組的話,只有當一個元組包含的所有元素都是可散列類型的情況下,它才是可散列的.
tt = (1, 2, (30, 40))
print(hash(tt))  # 8027212646858338501
tl = (1, 2, [30, 40])
# print(hash(tl))  # TypeError: unhashable type: 'list'
tf = (1, 2, frozenset([30, 40]))
print(hash(tf))  # 985328935373711578

"""
一般來講,用戶自定義的類型的對象都是可散列的,散列值就是它們的id()函數的返回值,所以所有這些對象在比較的時候都是不相等的.
如果一個對象實現了__eq__方法,並且在方法中用到了這個對象的內部狀態的話,那么只有當所有這些內部狀態都是不可變的情況下,這個對象踩死可散列的.
"""


class A:
    def __init__(self, a_):
        self.a = a_


class B:
    def __init__(self, a_):
        self.a = a_

    def __hash__(self):
        return hash(self.a)

    def __eq__(self, other):
        return hash(self) == hash(other)


a1 = A(1)
a2 = A([1, 2, 3])
print(hash(a1))  # -9223371857585079499
print(hash(a2))  # 179269859620

b1 = B(1)
b2 = B([1, 2])
print(hash(b1))  # 1
# print(hash(b2))  # TypeError: unhashable type: 'list'

# 根據這些定義,字典提供了很多種構造方法.
a = dict(one=1, two=2, three=3)
b = {'one': 1, 'two': 2, 'three': 3}
c = dict(zip(['one', 'two', 'three'], [1, 2, 3]))
d = dict([('one', 1), ('two', 2), ('three', 3)])
e = dict({'one': 1, 'two': 2, 'three': 3})
print(a == b == c == d == e)

# ------------------------------------字典推導式------------------------------------
# 字典推導可以從任何以鍵值對作為元素的可迭代對象中構建出字典
STUDENTS = [
    ("孫悟空", 100),
    ("豬八戒", 90),
    ("沙和尚", 80),
    ("二郎神", 70),
    ("哪吒", 60),
    ("諸葛亮", 50),
]
student = {name: number for name, number in STUDENTS}
print(student)  # {'孫悟空': 100, '豬八戒': 90, '沙和尚': 80, '二郎神': 70, '哪吒': 60, '諸葛亮': 50}
student2 = {name: number for name, number in student.items() if number > 60}
print(student2)  # {'孫悟空': 100, '豬八戒': 90, '沙和尚': 80, '二郎神': 70}

# ------------------------------------常見的映射方法------------------------------------
# dict.update(m, [**kargs])方法處理參數m的方式,是典型的'鴨子類型',函數首先檢查m是否有keys方法,如果有,那么update函數就把它當作映射對象來處理.
# 否則,函數會退一步,轉而把m當作包含了鍵值對(key, value)元素的迭代器.python里大多數映射類型的構造方法都采用了類似的邏輯,因此你既可以用一個映射對象來新建一個映射對象,
# 也可以用包含(key, value)元素的可迭代對象來初始化一個映射對象.
print(student)  # {'孫悟空': 100, '豬八戒': 90, '沙和尚': 80, '二郎神': 70, '哪吒': 60, '諸葛亮': 50}
student.update({"小白龍": 80, "唐僧": 90})
print(student)  # {'孫悟空': 100, '豬八戒': 90, '沙和尚': 80, '二郎神': 70, '哪吒': 60, '諸葛亮': 50, '小白龍': 80, '唐僧': 90}
student.update([("李世民", 90), ("朱元璋", 85)])
# {'孫悟空': 100, '豬八戒': 90, '沙和尚': 80, '二郎神': 70, '哪吒': 60, '諸葛亮': 50, '小白龍': 80, '唐僧': 90, '李世民': 90, '朱元璋': 85}
print(student)

# ------------------------------------用setdefault處理找不到的鍵------------------------------------
# 當字典d[k]不能找到正確的鍵的時候,Python會拋出異常,這個行為符合Python所信奉的'快速失敗'哲學.
# 也許每個Python程序員都知道可以用d.get(k, default)來代替d[k],給找不到的鍵一個默認的返回值.這比處理KeyError要方便不少.
# 但是要更新某個鍵對應的值的時候,不管用__getitem__還是get都會不自然,而且效率低.
# 例1
import re

WORD_RE = re.compile(r'\w+')
index = {}
with open('./word', encoding='utf-8') as fp:
    for line_no, line in enumerate(fp, 1):
        # re.finditer(string) 返回string中所有與pattern相匹配的全部字串,返回形式為迭代器。
        for match in WORD_RE.finditer(line):
            word = match.group()  # 匹配到的單詞
            column_no = match.start() + 1  # 單詞首字目所在的位置,從1開始
            location = (line_no, column_no)  # (行號, 列號)
            # 這其實是一種很不好的實現,這樣寫只是為了證明論點
            occurrences = index.get(word, [])  # 提取word出現的情況,如果還沒有它的記錄,返回[].
            occurrences.append(location)  # 把單詞出現的位置添加到列表的后面.
            index[word] = occurrences  # 把新的列表放回字典中,這又牽扯到一次查詢操作.
    # 以字母順序打印出結果
    # sorted函數的key=參數沒有調用str.upper,而是把這個方法的引用傳遞給sorted函數,這樣在排序的時候,單詞會被規范成統一格式.
    for word in sorted(index, key=str.upper):  # 將方法用作一等函數
        print(word, index[word])

index_ = {}

with open('./word', encoding='utf-8') as fp:
    for line_no, line in enumerate(fp, 1):
        for match in WORD_RE.finditer(line):
            word = match.group()
            column_no = match.start() + 1
            location = (line_no, column_no)
            index_.setdefault(word, []).append(location)
    for word in sorted(index_, key=str.upper):
        print(word, index_[word])
"""
dict.setdefault(key, []).append(new_value)
獲取單詞的出現情況列表,如果單詞不存在,把單詞和一個空列表放進映射,然后返回這個空列表,這樣就能在不進行第二次查找的情況下更新列表了.
也就是說,和下面的代碼效果一樣
if key not in dict:
    dict[key] = []
dict[key].append(new_value)
只不過,后者至少要進行兩次鍵查詢,如果鍵不存在的話,就是三次.而setdefault只需要一次就可以完成整個操作
"""

# ------------------------------------映射的彈性鍵查詢------------------------------------
"""
有時候為了方便起見,就算某個鍵在映射里不存在,我們也希望在通過這個鍵讀取值的時候能得到一個默認值.有兩個途徑能幫我們達到這個目的,一個是通過
defaultdict這個類型而不是普通的dict,另一個是給自己定義一個dict的子類,然后再子類中實現__missing__方法.
"""

"""
在用戶創建defalutdict對象的時候,就需要給它配置一個為找不到的鍵創造默認值的方法.
具體而言,在實例化一個defaultdict的時候,需要給構造方法提供一個可調用對象,這個可調用對象會在__getitem__碰到找不到的鍵的時候被調用,讓__getitem__
返回某種默認值
比如,我們新建了這樣一個字典:dd = defaultdict(list),如果鍵'new-key'在dd中還不存在的話,表達式dd['new-key']會按照以下的步驟來執行:
1.調用list()來建立一個新列表
2.把這個新列表作為值,'new-key'作為它的鍵,放到dd中
3.返回這個列表的引用
而這個用來生成默認值的可調用對象存放在名為default_factory的實例屬性中
"""
from collections import defaultdict

# 把list構造方法作為default_factory來創建一個defaultdict
index_dd = defaultdict(list)

with open('./word', encoding='utf-8') as fp:
    for line_no, line in enumerate(fp, 1):
        for match in WORD_RE.finditer(line):
            word = match.group()
            column_no = match.start() + 1
            location = (line_no, column_no)
            """
            如果index_dd並沒有word記錄,那么default_factory會被調用,為查詢步到的鍵創造一個值.
            這個值在這里是一個空列表,然后這個空列表會被賦值給index_dd[word],繼而被當作返回值返回.
            因此.append(location)操作總能成功
            """
            index_dd[word].append(location)
    for word in sorted(index_, key=str.upper):
        print(word, index_dd[word])

"""
如果在創建defaultdict的時候沒有指定default_factory,查詢不存在的鍵會觸發KeyError
defaultdict里的default_factory只會在__getitem__里被調用,在其他的方法里完全不會發揮作用.
比如dd[k]這個表達式會調用default_factory創造某個默認值,而dd.get(k)則會返回None
"""

# 所有這一切背后的功臣其實是特殊方法__missing__.它會在defaultdict遇到找不到的鍵的時候調用default_factory,
# 而實際上這個特性是所有映射類型都可以選擇去支持的.
"""
所有的映射類型在處理找不到的鍵的時候,都會牽扯到__missing__方法.這也是這個方法被稱作''missing的原因.
雖然基類dict沒有定義這個方法,但是dict是知道有這個東西存在的.也就是說,如果有一個類繼承了dict,然后這個繼承類提供了__missing__方法,
那么在__getitem__碰到找不到的鍵的時候,Python就會自動調用它,而不是拋出KeyError異常.
__missing__方法只會被__getitem__調用,對get或者__contains__這些方法的使用沒有影響.
"""


class StrKeyDict(dict):  # 繼承dict
    """
    如果要自定義一個映射類型,更合適的策略其實是繼承collections.UserDict類.這里我們從dict繼承,
    只是為了演示__missing__是如何被dict.__getitem__調用的
    """

    def __missing__(self, key):
        """
        為什么isinstance(key, str)是必須的?
        如果沒有這個測試,當str(key)不是一個存在的鍵,代碼就會陷入無限遞歸.這是因為__missing__的最后一行中的self[str(key)]會調用
        __getitem__,而這個str(key)又不存在,於是__missing__又會被調用.
        """
        if isinstance(key, str):  # 如果找不到的鍵本身就是字符串,那就拋出KeyError異常
            raise KeyError(key)
        # 如果找不到的鍵不是字符串,那么就把它轉換成字符串再進行查找
        return self[str(key)]

    def get(self, key, default=None):
        """
        get方法把查找工作用self[key]的形式委托給__getitem__,這樣在宣布查找失敗之前,還能通過__missing__再給某個鍵一個機會
        """
        try:
            return self[key]
        except KeyError:
            # 如果拋出KeyError,那么說明__missing__也失敗了,於是返回default.
            return default

    def __contains__(self, item):
        """
        為了保持一致性,__contains__方法也是必須的.這是因為k in d這個操作會調用它,但是我們從dict繼承到的__contains__方法[不會]在找不到鍵的時候
        調用__missing__方法.__contains__里還有個細節,就是我們這里沒有用更具Python風格的方式--item in self--來檢驗是否存在,因為那也會導致
        __contains__被遞歸調用,為了避免這一情況,這里采取了更顯式的方法,直接在這個self.keys()里查詢.

        像k in my_dict.keys()這種操作在Python3中是很快的,而且即便映射類型對象很龐大也沒關系.這因為dict.keys()的返回值是一個"視圖".
        視圖就像一個集合,而且跟字典類似的是,在視圖里查找一個元素的速度很快.
        Python2中的dict.keys()返回的則是一個列表,它在處理體積大的對象的時候效率不會太高,因為k in my_list操作需要掃描整個列表.
        """
        # 先安裝傳入鍵的原本的值來查找,如果沒找到,再用str()方法把鍵轉換成字符串再查找一次.
        return item in self.keys() or str(item) in self.keys()


d = StrKeyDict([('2', 'two'), ('4', 'four')])
print(d['2'])  # two
print(d[4])  # four
# print(d[1])  # KeyError: '1'

print(d.get('2'))  # two
print(d.get(4))  # four
print(d.get(1))  # None

print(2 in d)  # True
print(1 in d)  # False

# ------------------------------------字典的變種------------------------------------
from collections import OrderedDict, ChainMap, Counter, UserDict

"""
collections.OrderedDict
這個類型在添加鍵的時候會保持順序,因此鍵的迭代次序總是一致的.OrderedDict的popitem方法默認刪除並返回的是字典里的最后一個元素.
但是如果像my_dict.popitem(last=False)這樣調用它,那么它刪除並返回第一個被添加進去的元素.

collections.ChainMap
該類型可以容納數個不同的映射對象,然后在進行鍵查找操作的時候,這些對象會被當作一個整體被逐個查找,直到鍵被找到為止.
這個功能在給有嵌套作用域的語言做解釋器的時候很有用,可以用一個映射對象來代表一個作用域的上下文.
例如下面這個Python變量查詢規則:
"""
import builtins

py_lookup = ChainMap(locals(), globals(), vars(builtins))

"""
collections.Counter
這個映射類型會給鍵准備一個整數計數器.每次更新一個鍵的時候都會增加這個計數器.所以這個類型可以用來給可散列列表對象計數,
或者是當成多重集來用--多重集合就是集合里的元素可以出現不止一次.Counter實現了+和-運算符用來合並記錄,還有像most_common([n])這類很有用的方法.
most_common([n])會按照次序返回映射里最常見的n個鍵和它們的計數.
下面的小例子利用Counter來計算單詞中各個字母出現的次數:
"""
ct = Counter('abracadabra')
print(ct)  # Counter({'a': 5, 'b': 2, 'r': 2, 'c': 1, 'd': 1})
ct.update('aaaaazzz')
print(ct)  # Counter({'a': 10, 'z': 3, 'b': 2, 'r': 2, 'c': 1, 'd': 1})
print(ct.most_common(2))  # [('a', 10), ('z', 3)]

"""
collections.UserDict
這個類其實就是把標准的dict用純Python又實現了一遍.跟前三者不同,UserDict是讓用戶繼承寫子類的.下面就來試試:

就創造自定義映射類型來說,以UserDict為基類,總比以普通的dict為基類要來得方便.這體現在,我們能夠改進上面的StrKeyDict類,使得所有的鍵都存儲為字符串類型.
而更傾向於從UserDict而不是從dict繼承的主要原因是,后者有時會在某些方法的實現上走一些捷徑,導致我們不得不在它的子類中重寫這些方法,但是UserDict就不會帶來這些問題.
另外一個需要注意的地方是UserDict並不是dict的子類,但是UserDict有一個叫做data的屬性,是dict的實例,這個屬性實際上是UserDict最終存儲數據的地方.
這樣做的好處是,比起上面的例子,UserDict的子類就能在實現__setitem__的時候避免不必要的遞歸,也可以讓__contains__里的代碼更簡潔.

下面的雷子不但把所有的鍵都以字符串的形式存儲,還能處理一些創建或者更新實例時包含非字符串類型的鍵這類意外情況.
"""


class StrKeyDict_U(UserDict):  # StrKeyDict_U是對UserDict的拓展
    def __missing__(self, key):
        if isinstance(key, str):
            raise KeyError(key)
        return self[str(key)]

    def __setitem__(self, key, value):
        # 把所有的鍵都轉換成字符串.由於把具體的實現委托給了self.data屬性,這個方法寫起來也不難.
        self.data[str(key)] = value

    def __contains__(self, item):
        # 這里可以放心假設所有已經存儲的鍵都是字符串.
        # 並且可以直接在self.data上查詢
        return str(item) in self.data


"""
因為UserDict繼承的是MutableMapping,所以StrKeyDict_U里剩下的那些映射類型的方法都是從UserDict、MutableMapping和Mapping這些超類繼承來的.
特別是最后的Mapping類,它雖然是一個抽象基類(ABC),但它卻提供了好幾個實用的方法.
MutableMapping.update
這個方法不但可以為我們所直接利用,它還用在__init__里,然構造方法可以利用出入的各種參數(其他映射類型、元素是(key, value)對的可迭代對象和鍵值參數)
來新建實例.因為這個方法在背后是用self[key] = value來添加新值的,所以它其實是在使用我們的__setitem__方法

Mapping.get
在StrKeyDict中,我們不得不改寫get方法,好讓它的表現跟__getitem__一致.而在StrKeyDict_U中就沒有這個必要了,因為它繼承了Mapping.get方法,
這個方法的實現方式跟StrKeyDict.get是一模一樣的.
"""

# ------------------------------------不可變映射類型------------------------------------
"""
標准庫里的所有映射類型都是可變的,但是有時候我們需要限制用戶的修改
從Python3.3開始,types模塊中引入了一個封裝類名叫MappingProxyType.如果給這個類一個映射,它會返回一個只讀的映射視圖.
雖然是個只讀視圖,但是它是動態的.這意味着如果對原映射做出了改動,我們通過這個視圖可以觀察到,但是無法通過這個視圖對原映射做修改.
"""
from types import MappingProxyType
d = {1: 'A'}
d_proxy = MappingProxyType(d)
print(d_proxy)  # {1: 'A'}
print(d_proxy[1])  # A   d中的內容可以通過d_proxy看到
# 但是通過d_proxy並不能做任何修改
# d_proxy[2] = 'X'  # TypeError: 'mappingproxy' object does not support item assignment
d[2] = 8
# d_proxy是動態的,也就是說對d所做的任何改動都會反饋到它的上面
print(d_proxy[2])  # 8


免責聲明!

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



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