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
posted @ 2020-06-19 16:05  怀心抱素  阅读(1646)  评论(0编辑  收藏  举报