第3章 字典和集合

本文介绍了Python中的字典推导、映射方法如defaultdict和__missing__,以及字典变种如OrderedDict和Counter。重点讲解了如何处理映射中键查询、集合的去重与运算,以及dict和set的底层实现原理。
摘要由CSDN通过智能技术生成

第3章 字典和集合

3.1 泛映射类型

collections.abc模块中有MappingMutableMapping两个抽象基类, 为dict和其他类似的类型定义形式接口.

非抽象映射类型一般不直接继承这些抽象基类, 他们会直接对dictcollections.Userdict进行扩展. 这些抽象基类的主要作用是作为形式化的文档, 它们定义了构建一个映射类型所需要的最基本的借口.

from collections import abc
my_dict = {}
isinstance(my_dict, abc.Mapping)

True

标准库里的所有映射类型都是利用dict来实现的, 他们的共同限制是可散列的数据类型才能作为映射里的键.

原子不可变数据类型(str,byte和数值类型)都是可散列类型, frozenset也是可散列的. 元组中的所有元素都是可散列类型时, 元组是可散列的.

3.2 字典推导

字典推导可以从任何以键值对作为元素的可迭代对象中构建出字典.

isinstance(my_dict, abc.Mapping)
#%%
DIAL_CODES = [
    (86, 'China'),
    (91, 'India'),
    (1, 'United States'),
    (62, 'Indonesia'),
    (55, 'Brazil'),
    (92, 'Pakistan'),
    (880, 'Bangladesh'),
    (234, 'Nigeria'),
    (7, 'Russia'),
    (81, 'Japan'),
]
country_code = {country : code for code, country in DIAL_CODES}
country_code
{'China': 86,
 'India': 91,
 'United States': 1,
 'Indonesia': 62,
 'Brazil': 55,
 'Pakistan': 92,
 'Bangladesh': 880,
 'Nigeria': 234,
 'Russia': 7,
 'Japan': 81}
{code : country.upper() for country, code in country_code.items() if code < 66}

{1: ‘UNITED STATES’, 62: ‘INDONESIA’, 55: ‘BRAZIL’, 7: ‘RUSSIA’}

3.3 常见的映射方法

在映射对象的方法里, setdefault是比较微妙的一个.

当字典d[k]找不到正确的键时, Python会抛出异常. 可以使用d.get(k, default)来代替d[k], 给找不到的键一个默认的返回值. 然而这不是最好的方法.

import sys
import re

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

index = {}
with open(sys.argv[1], 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)
            # bad example
            occurrences = index.get(word, [])
            occurrences.append(location)
            index[word] = occurrences

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

上述的这种写法是一种不好的写法, 用setdefault可以更好的解决

import sys
import re

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

index = {}
with open(sys.argv[1], 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])

这种情况下, 如果一个单词不存在, 会把单词和一个空列表放进映射.

3.4 映射的弹性键查询

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

在实例化一个defaultdict时, 需要给构造方法一个可调用对象, 这个可调用对象会在__getitem__碰到找不到的键的时候被调用.

import collections
import sys
import re

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

index = collections.defaultdict(list)
with open(sys.argv[1], 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[word].append(location)

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

上述例子把list作为default_factory来创建defaultdict.

这里的default_factory只有在__getitem__时会被调用, 其他方法如get时则不会被调用.

3.4.2 特殊方法__missing__

__missing__方法同样只会在__getitem__时调用.

class StrKeyDict0(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()
from strkeydict0 import StrKeyDict0
d = StrKeyDict0([('2', 'two'), ('4', 'four')])
d['2']

‘two’

d[4]

‘four’

d[1]

KeyError: ‘1’

d.get('2')

‘two’

d.get(4)

‘four’

d.get(1,' N/A')

’ N/A’

2 in d

True

1 in d

False

3.5 字典的变种

collections.OrderedDict: 添加键的时候会保持顺序.

collections.ChainMap: 容纳数个不同的映射对象.

collections.Counter: 给键一个整数计数器. most_common方法会返回最常见的前n个键以及他们的计数.

collections.UserDict把标准dict用纯python实现了一遍.

3.6 子类化UserDict

就创建自定义映射类型来说, 以UserDict为基类更方便.

dict继承不好是因为dict有时会在某些方法上走捷径, 导致我们要在其子类中重写这些方法.

UserDict并不是dict的子类, 但它的一个属性datadict的实例. 这个属性实际上是UserDict最终存储数据的地方.

import collections


class StrKeyDict(collections.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, item):
        self.data[str(key)] = item

UserDict继承的是MutableMapping, 所以StrKeyDict剩下的那些映射类型的方法都是从UserDict, MutableMapping, Mapping这些超类继承而来的.

3.7 不可变映射类型

types模块中有一个封装类名叫MappingProxyType, 如果给这个类一个映射, 它会返回一个只读的映射视图.

from types import MappingProxyType

d = {1: 'A'}
d_proxy = MappingProxyType(d)
print(d_proxy)
print(d_proxy[1])
d_proxy[2] = 'x'

{1: ‘A’}
A

TypeError: ‘mappingproxy’ object does not support item assignment

d[2] = 'B'
print(d_proxy)
print(d_proxy[2])

{1: ‘A’, 2: ‘B’}
B

3.8 集合论

集合可以用来去重

l = ['spam', 'spam', 'eggs', 'spam']
print(set(l))
print(list(set(l)))

{‘spam’, ‘eggs’}
[‘spam’, ‘eggs’]

a = {1, 2, 3}
b = {2, 3, 4}
print(a | b)
print(a & b)
print(a - b)

{1, 2, 3, 4}
{2, 3}
{1}

上述例子展示了集合的交, 并, 差运算.

查找needleshaystack中出现的次数, 可以用

found = len(needles & haystack)

3.8.1 集合字面量

如果是空集, 必须写成set()的形式, 否则可以写成{1, 2} 的形式.

如果写成{}, 创建的其实是一个空字典.

s = {1}
print(type(s))
print(s)
print(s.pop())
print(s)

<class ‘set’>
{1}
1
set()

{1, 2, 3}这样的字面量句法要比构造方法set([1, 2, 3])更快, 通过dis.dis可以查看字节码.

from dis import dis
dis('{1}')
1           0 LOAD_CONST               0 (1)
              2 BUILD_SET                1
              4 RETURN_VALUE
dis('set([1])')
 1           0 LOAD_NAME                0 (set)
              2 LOAD_CONST               0 (1)
              4 BUILD_LIST               1
              6 CALL_FUNCTION            1
              8 RETURN_VALUE

frozenset没有特殊字面量句法, 只能采用构造方法.

frozenset(range(10))

frozenset({0, 1, 2, 3, 4, 5, 6, 7, 8, 9})

3.8.2 集合推导

from unicodedata import name
{chr(i) for i in range(32, 256) if 'SIGN' in name(chr(i), '')}

{‘#’,
‘$’,
‘%’,
‘+’,
‘<’,
‘=’,
‘>’,
‘¢’,
‘£’,
‘¤’,
‘¥’,
‘§’,
‘©’,
‘¬’,
‘®’,
‘°’,
‘±’,
‘µ’,
‘¶’,
‘×’,
‘÷’}

3.9 dictset背后

3.9.1 一个关于效率的实验

在字典和集合中搜索要比在列表中快很多.

3.9.2 字典中的散列表

散列表其实是一个稀疏数组, 其单元通常称为表元. 在dict的散列表中, 每个键值对都占用一个标原, 每个表元有两个部分, 对键的引用和对值的引用. 所有表元大小一致, 因此可以用偏移量读取某个单元.

Python会设法保证大概三分之一的表元是空的, 所以快达到阈值时, 原有散列表会被复制一个更大的空间里面.

要将一个对象放入散列表, 首先要计算这个元素键的散列值. hash()可以用来计算.

1.散列值和相等性

要想散列值能够胜任散列表索引这一角色, 他们必须在索引空间尽量分散, 越是相似的对象, 散列值的差异应越大.

2.散列表算法

要获取my_dict[search_key]的值, Python首先调用hash(search_key)计算其散列值, 把这个值最低几位当作偏移量, 在散列表里查找表元.

若表元为空, 则抛出KeyError异常. 若不为空, 则检查search_key==found_key, 若相等则返回found_value. 若不等, 则在散列值中另外取几位, 再次尝试.

添加新元素只需在发现空表元时放入新元素. 更新现有键值在找到相对应表元后, 原值会被替换成新值.

3.9.3dict的实现及其导致的后果

1.键必须是可散列的

一个可散列对象需满足以下要求

(1) 支持hash函数, 并且通过__hash__方法得到的散列值是不变的.

(2) 支持通过__eq__方法检测相等性

(3) a==b为真, hash(a)==hash(b)为真

2.字典在内存上开销巨大

如果要存放数量巨大的记录, 那么放在由元组或是具名元组构成的列表中会是一个比较好的选择.

3.键查询快
4.键的次序取决于添加顺序

dict中添加新键发生散列冲突时, 新键可能会被安排存放到另一位置. 因此在dict([(key1, value1), (key2, value2)])的比较中得到的结果是相等.

5.往字典添加新键可能改变已有键的顺序

无论何时往字典添加新键, Python都可能做出字典扩容的决定,这个过程可能产生新的散列冲突, 导致新散列表中键的次序变化.

因此, 不要对字典同时进行迭代和修改.

3.9.4 set的实现以及导致的后果

集合里的元素是可散列的

集合很消耗内存

可以高效判断元素是否存在于某个集合

元素的次序取决于被添加到集合的次序

往集合里添加元素, 可能会改变已有元素的次序

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值