映射是编程中绕不过去的数据结构,Python 中的映射脱胎自 Mapping
和其超集 MutableMapping
,Mapping
又继承自 Collection
,和序列类相似。所以 Python 中的映射类型与序列类型有很多相似的操作。
import collections.abc as abc
help(abc.Mapping)
help(abc.MutableMapping)
Python 里最常见的映射就是 dict 了,这种内置类型由 C 实现,性能很高,而且封装了很多实用操作。简单举例
a = {'a':'A','b':'B'}
a.clear()
a
obj_dict = {'a':'A','b':'B'}
for key, val in obj_dict.items():
print(key, val)
lst_key = ['a','b']
same_val = ['A','B']
obj_dict = dict.fromkeys(lst_key,same_val)
obj_dict
other_dict = obj_dict.copy()
other_dict['a'][0] = 'C'
print(obj_dict, other_dict)
import copy
other_dict = copy.deepcopy(obj_dict)
other_dict['a'][0] = 'A'
print(obj_dict, other_dict)
obj_dict.get('c')
obj_dict['c']
obj_dict.setdefault('c','C')
obj_dict
obj_dict.setdefault('a','A')
obj_dict
obj_dict.update((('a','A'),))
obj_dict
obj_dict.update({'d':'D'})
obj_dict
obj_dict.update(e='E',f='F')
obj_dict
如果想要自己封装一个字典,不建议继承 dict
(list
同理),子类的覆盖方法有时不会生效
class ErrDict(dict):
def __setitem__(self, key, value):
super().__setitem__(key, value*2)
err_dict = ErrDict(one = 1) # 覆盖方法失效
print(err_dict)
err_dict['two'] = 2 # 覆盖方法有效
print(err_dict)
比较好的继承对象是 collections.Userdict
,但更推荐通过实例化 collections.defaultdict
来构建自己的字典,在其内部__getitem__
的实现中通过重写 __missing__
方法,能做到当查询键不存在于字典中时,不报异常而是走缺失处理程序
from collections import defaultdict
my_dict = defaultdict(dict)
my_dict['key']
常和字典一起说的是集合 set
,Python 里的集合能执行集合运算。此外还有一个 frozenset
,顾名思义 frozenset 的值初始化后就不可更改了,所以很适合做字典的 key
f_set = frozenset('abc')
n_set = set('abc')
print(n_set, f_set)
而如果深究实现机制,集合就是字典的特例,因为字典中对键 key
的要求完全符合集合的特点,所以集合可以用所有 key 的 val 都为空的字典封装实现。
字典的实现一般是基于哈希法,查询时先对键求散列值,定位到散列表的一个表元,如果此时表元为空则进入异常处理流程。如果表元有值,因为存在不同元素散列值相同的情形,需要确认 key
是否相同,不同则进入哈希冲突处理流程,比如再散列,链地址等,相同则正常返回。Python 内置 hash()
方法获取对象哈希值,也可以自己实现__hash__
方法来自定义哈希规则
哈希函数因为是通过索引直接访问表元,故读取效率极高,只有$O(1)$的时间复杂度,但内存占用也会大些。与之相比,list
为代表的序列类型往往效率会低很多。Python 对自己的内部对象和用户自定义对象都用 dict 进行了封装。
同时为了确保唯一性,参与哈希计算的元素必须不可变,也就导致 key
只能是 str
tuple
这类不可变对象。
说对象肯定得提一提变量。Java,C# 中的变量申明时会指定类型,初始化时计算机去内存开辟一块空间打上类型标识,从此变量就和这块空间绑定在了一起。Python 作为动态语言,变量和对象间的关系没有这么亲密,Python 变量实质只是一个指针,对象初始化完毕后,使变量指针指向对象的内存空间,所以 Python 变量更像是对象的别名
one_lst = [1,2,3]
two_lst = one_lst
two_lst.append(4) # 指针指向位置相同
one_lst
变量行为上,我们曾经提到过 ==
和 is
的区别,前者实质是调用 __eq__
方法进行值比对,is
则是比较两变量指向的内存空间是否一致。Python 在这种普遍规律上做一定的优化,如果是小整数(-5~256),短字符串,每次声明新变量时,Python 不会构建新对象而是会寻找上一个值相同的对象然后返回。
num_obj = 2
another_num_obj = 2
print(id(num_obj), id(another_num_obj)) # 小整数指向相同
str_obj = 'abc'
another_str_obj = 'abc'
print(id(str_obj),id(another_str_obj))
有创建就有删除,Python 提供了 del
方法来删除变量。实质上,因为变量只是对象别名的缘故,Python 采用的是引用计数的方法来判断是否释放空间,执行del(var_name)
时会将var_name
指针释放,指针指向对象内部维护的引用计数减一,引用计数为 0 时释放对象占据的内存空间。
对于自定义的对象,可以通过配置__del__
方法告知解释器在回收该对象时要做些什么。
a = 'abc'
b = a
del a # 释放指针,引用计数减一
b # 计数不为零依旧可访问
可变性除了变量自身,对象本身的可变性有时也会给人带来困扰。比如下面这个例子
def add(factor_one, factor_two):
factor_one += factor_two
return factor_one
param1 = [1,2]
param2 = [3,4]
print(add(param1, param2), param1, param2)
这里定义了一个加法函数 add
,执行两个加数的求和运算。传入了两个列表参数后,其中一个加数param1
也被改变了。
造成这一问题的原因是 list
本身就是一种可变对象,传递参数时将自己的引用传入,而 +=
本身是一种就地变换,导致 param1
实际既是参数又是结果。
所以编程实践中不要使用可变对象作为参数,也不要令默认参数为可变对象