我是靠谱客的博主 糟糕红牛,最近开发中收集的这篇文章主要介绍第三章(提炼)字典和集合(二),觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

一. 映射的弹性键查询

        某个键在映射里不存在,我们也希望在通过这个键读取值的时候能得到一个默认值。有两个途径能帮我们达到这个目的,一个是通过 defaultdict 这个类型而不是普通的 dict,另一个是给自己定义一个 dict 的子类,然后在子类中实现 __missing__ 方法。

1.1 defaultdict:处理找不到的键的一个选择

        在实例化一个 defaultdict 的时候,需要给构造方法提供一个可调用对象,这个可调用对象会在 __getitem__ 碰到找不到的键 的时候被调用,让 __getitem__ 返回某种默认值。

演示1  个用来生成默认值的可调用对象存放在名为 default_factory 的实例属性里

演示2  利用 defaultdict 实例而不是setdefault 方法(还是上一节的示例,改用defaultdict代替setdefault方法)

"""创建一个从单词到其出现情况的映射"""
import sys
import re
import collections

WORD_RE = re.compile(r'w+')

index = collections.defaultdict(list)
with open(sys.argv[1], encoding='utf-8') as f:
    for line_no, line in enumerate(f, 1):
        for match in WORD_RE.finditer(line):
            word = match.group()
            column_no = match.start()+1
            location = (line_no, column_no)
            index[word].append(location)

for word in sorted(index, key=str.upper):
    print(word, index[word])

        如果 index 并没有 word 的记录,那么 default_factory 会被调 用,为查询不到的键创造一个值。这个值在这里是一个空的列表,然后这个空列表被赋值给 index[word],继而被当作返回值返回,因此 .append(location) 操作总能成功。

注意:

  1. 如果在创建 defaultdict 的时候没有指定 default_factory ,查询不存在的键会触发 KeyError
  2. defaultdict 里的 default_factory 只会在 __getitem__ 里被调用,在其他的方法里完全不会发挥作用。
  3. 如, dd 是个 defaultdict k 是个找不到的键, dd[k] 这个表达式会调用 default_factory 创造某个默认值,而 dd.get(k) 则会 返回 None
  4. 所有这一切背后的功臣其实是特殊方法 __missing__。它会在  defaultdict 遇到找不到的键的时候调用 default_factory ,而实际 上这个特性是所有映射类型都可以选择去支持的。

1.2 特殊方法__missing__

        如果有一个类继承了 dict,然后这个继承类提供了 __missing__ 方法,那么在 __getitem__ 碰到找不到的键的时候,Python 就会自动调用它, 而不是抛出一个 KeyError 异常。__missing__ 方法只会被 __getitem__ 调用(比如在表达式 d[k] 中)。提供 __missing__ 方法对 get 或者 __contains__(in 运算符会用到这个方法)这些方法的使用没有影响。

演示3  自定义映射类型,在查询的时候把非字符串的键转换为字符串

class StrKeyDict(dict):

    def __missing__(self, key):
        if isinstance(key, str):
            raise KeyError(key)
        return self[str(key)]

    def get(self, key, default=None):
        try:
            return self[key]
        except KeyError:
            return default

    def __contains__(self, key):
        return key in self.keys() or str(key) in self.keys()

  1. 为什么 isinstance(key, str) 测试在上面的 __missing__ 中是必需的?如果没有这个测试,只要 str(k) 返回的是一个存在的键,那么 __missing__ 方法是没问题的,不管是字符串键还是非字符串键,它都能正常运行。但是如果 str(k) 不是一个存在的键,代码就会陷入无限递归。这是因为 __missing__ 的最后一行中的 self[str(key)] 会调用 __getitem__ ,而这个 str(key) 又不存在,于是 __missing__ 又会被调用。
  2. 为了保持一致性,__contains__ 方法在这里也是必需的。这是因为 k  in d 这个操作会调用它,但是我们从 dict 继承到的 __contains__ 方法不会在找不到键的时候调用 __missing__ 方法。 __contains__ 里还有个细节,就是我们这里没有用更具 Python 风格的方式 —— k in my_dict—— 来检查键是否存在,因为那也会导致 __contains__ 被递归调用。为了避免这一情况,这里采取了更显式的方法,直接在这个 self.keys() 里查询。
  3. 像 k in my_dict.keys() 这种操作在 Python 3 中是很快 的,而且即便映射类型对象很庞大也没关系。这是因为  dict.keys() 的返回值是一个 视图 。视图就像一个集合,而且跟 字典类似的是,在视图里查找一个元素的速度很快。

1)__getitem__在获取键失败时,会自动调用__missing__方法

2)get 方法把查找工作用 self[key] 的形式委托给 __getitem__,这样在宣布查找失败之前,还能通过 __missing__ 再给某个键一个机会:

3)为了保持一致性,__contains__ 方法在这里也是必需的:

二. 字典的变种

这一节总结了标准库里 collections 模块中,除了 defaultdict 之外的不同映射类型。

3.1 collections.OrderedDict

        这个类型在添加键的时候会保持顺序,因此键的迭代次序总是一致的。OrderedDict popitem 方法默认删除并返回的是字典里的最后一个元素,但是如果像 my_odict.popitem(last=False) 这样调用它,那么它删除并返回第一个被添加进去的元素。

3.2 collections.ChainMap

        该类型可以容纳数个不同的映射对象,然后在进行键查找操作的时候,这些对象会被当作一个整体被逐个查找,直到键被找到为止。

3.3 collections.Counter

        这个映射类型会给键准备一个整数计数器。每次更新一个键的时候都会增加这个计数器。所以这个类型可以用来给可散列表对象计数,或者是当成多重集合来用——多重集合就是集合里的元素可以出现不止一次。Counter 实现了 + - 运算符用来合并记录,以及 most_common([n]) 这类很有用的方法。most_common([n]) 会按照次序返回映射里最常见的 n 个键和它们的计数。

演示4  利用Counter来计算单词中各个字母出现的次数

3.4 collections.UserDict

         这个类其实就是把标准 dict 用纯 Python 又实现了一遍。跟 OrderedDictChainMap Counter 这些开箱即用的类型不 同,UserDict 是让用户继承写子类的。

三. 子类化UserDict

        就创造自定义映射类型来说,以 UserDict 为基类,总比以普通的 dict 为基类要来得方便。而更倾向于从 UserDict 而不是从 dict 继承的主要原因是,后者有时会在某些方法的实现上走一些捷径,导致我们不得不在它的子类中重写这些方法,但是UserDict 就不会带来这些问题。

        另外一个值得注意的地方是,UserDict 并不是 dict 的子类,但是UserDict 有一个叫作 data 的属性,是 dict 的实例,这个属性实际上是 UserDict 最终存储数据的地方。

演示5  自定义映射类型,无论是添加、更新还是查询操作,StrKeyDict 都会把非字符串的键转换为字符串

from collections import UserDict

class StrKeyDict(UserDict):
    def __missing__(self, key):
        if isinstance(key, str):
            raise KeyError(key)
        return self[str(key)]

    def __contains__(self, key):
        return str(key) in self.data

    def __setitem__(self, key, value):
        self.data[str(key)] = value

与直接继承dict相比,此处继承UserDict有以下明显的优势:

  1. __contains__ 则更简洁些。这里可以放心假设所有已经存储的键都是字符串。因此,只要在 self.data 上查询就好了,并不需要去麻烦 self.keys();
  2. __setitem__ 会把所有的键都转换成字符串。由于把具体的实现委 托给了 self.data 属性,这个方法写起来也不难。
  3.  因为 UserDict 继承的是 MutableMapping,所以 StrKeyDict 里剩下的那些映射类型的方法都是从 UserDict、MutableMapping 和 Mapping 这些超类继承而来的。特别是最后的 Mapping 类,它虽然是一个抽象基类(ABC),但它却提供了好几个实用的方法。以下两个方法值得关注:
    1. MutableMapping.update这个方法不但可以为我们所直接利用,它还用在 __init__ 里,让构造方法可以利用传入的各种参数(其他映射类型、元素是 (key, value) 对的可迭代对象和键值参数)来新建实例。因为这个方法在背后是用 self[key] = value 来添加新值的,所以它其实是在使用我们的 __setitem__ 方法。
    2. Mapping.get:在直接继承dict时,我们不得不改写 get 方法,好让它的表现跟 __getitem__ 一致。而继承UserDict后无需重写get方法,因为它继承了 Mapping.get 方法,会将操作委托给__getitem__:
class Mapping(Collection):
    ...

    @abstractmethod
    def __getitem__(self, key):
        raise KeyError

    def get(self, key, default=None):
        'D.get(k[,d]) -> D[k] if k in D, else d.  d defaults to None.'
        try:
            return self[key]
        except KeyError:
            return default

    ...

在控制台导入上述的StrKeyDict,初始化时传入的键均为数值类型,最终存储的却是字符串类型。通过setdeafult设置也是一样的,需要注意的是setdefault设置值后,还会将value返回: 

四. 不可变映射类型

        标准库里所有的映射类型都是可变的,但有时候你会有这样的需求,比如不能让用户错误地修改某个映射。从 Python 3.3 开始,types 模块中引入了一个封装类名叫 MappingProxyType。如果给这个类一个映射,它会返回一个只读的映射视图。虽然是个只读视图,但是它是动态的。这意味着如果对原映射做出了改动,我们通过这个视图可以观察到,但是无法通过这个视图对原映射做出修改。

演示6  用 MappingProxyType 来获取字典的只读实例 mappingproxy

        d 中的内容可以通过 d_proxy 看到,但是通过 d_proxy 并不能做任何修改。d_proxy 是动态的,也就是说对 d 所做的任何改动都会反馈到它上面。

最后

以上就是糟糕红牛为你收集整理的第三章(提炼)字典和集合(二)的全部内容,希望文章能够帮你解决第三章(提炼)字典和集合(二)所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(48)

评论列表共有 0 条评论

立即
投稿
返回
顶部