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 = repile(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 = up() # 匹配到的单词
column_no = match.start() + 1 # 单词⾸字⽬所在的位置,从1开始
location = (line_no, column_no) # (⾏号, 列号)
# 这其实是⼀种很不好的实现,这样写只是为了证明论点
occurrences = (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 = up()
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 = up()
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--来检验是否存在,因为那也会导致python index函数
__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'
('2')) # two
(4)) # four
(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})
st_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__⽅法
<
在StrKeyDict中,我们不得不改写get⽅法,好让它的表现跟__getitem__⼀致.⽽在StrKeyDict_U中就没有这个必要了,因为它继承了⽅法,
这个⽅法的实现⽅式跟是⼀模⼀样的.
"""
# ------------------------------------不可变映射类型------------------------------------
"""
标准库⾥的所有映射类型都是可变的,但是有时候我们需要限制⽤户的修改
从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
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论