Python3入门到精通——dict、set 和可变性

作者: Daniel Meng

GitHub: LibertyDream

博客:明月轩

本系列教程采用知识共享署名-非商业性使用-相同方式共享 2.5 中国大陆许可协议

映射是编程中绕不过去的数据结构,Python 中的映射脱胎自 Mapping 和其超集 MutableMappingMapping 又继承自 Collection,和序列类相似。所以 Python 中的映射类型与序列类型有很多相似的操作。

In [1]:
import collections.abc as abc

help(abc.Mapping)
Help on class Mapping in module collections.abc:

class Mapping(Collection)
 |  Method resolution order:
 |      Mapping
 |      Collection
 |      Sized
 |      Iterable
 |      Container
 |      builtins.object
 |  
 |  Methods defined here:
 |  
 |  __contains__(self, key)
 |  
 |  __eq__(self, other)
 |      Return self==value.
 |  
 |  __getitem__(self, key)
 |  
 |  get(self, key, default=None)
 |      D.get(k[,d]) -> D[k] if k in D, else d.  d defaults to None.
 |  
 |  items(self)
 |      D.items() -> a set-like object providing a view on D's items
 |  
 |  keys(self)
 |      D.keys() -> a set-like object providing a view on D's keys
 |  
 |  values(self)
 |      D.values() -> an object providing a view on D's values
 |  
 |  ----------------------------------------------------------------------
 |  Data and other attributes defined here:
 |  
 |  __abstractmethods__ = frozenset({'__getitem__', '__iter__', '__len__'}...
 |  
 |  __hash__ = None
 |  
 |  __reversed__ = None
 |  
 |  ----------------------------------------------------------------------
 |  Class methods inherited from Collection:
 |  
 |  __subclasshook__(C) from abc.ABCMeta
 |      Abstract classes can override this to customize issubclass().
 |      
 |      This is invoked early on by abc.ABCMeta.__subclasscheck__().
 |      It should return True, False or NotImplemented.  If it returns
 |      NotImplemented, the normal algorithm is used.  Otherwise, it
 |      overrides the normal algorithm (and the outcome is cached).
 |  
 |  ----------------------------------------------------------------------
 |  Methods inherited from Sized:
 |  
 |  __len__(self)
 |  
 |  ----------------------------------------------------------------------
 |  Methods inherited from Iterable:
 |  
 |  __iter__(self)

In [2]:
help(abc.MutableMapping)
Help on class MutableMapping in module collections.abc:

class MutableMapping(Mapping)
 |  Method resolution order:
 |      MutableMapping
 |      Mapping
 |      Collection
 |      Sized
 |      Iterable
 |      Container
 |      builtins.object
 |  
 |  Methods defined here:
 |  
 |  __delitem__(self, key)
 |  
 |  __setitem__(self, key, value)
 |  
 |  clear(self)
 |      D.clear() -> None.  Remove all items from D.
 |  
 |  pop(self, key, default=<object object at 0x000002474D02E0F0>)
 |      D.pop(k[,d]) -> v, remove specified key and return the corresponding value.
 |      If key is not found, d is returned if given, otherwise KeyError is raised.
 |  
 |  popitem(self)
 |      D.popitem() -> (k, v), remove and return some (key, value) pair
 |      as a 2-tuple; but raise KeyError if D is empty.
 |  
 |  setdefault(self, key, default=None)
 |      D.setdefault(k[,d]) -> D.get(k,d), also set D[k]=d if k not in D
 |  
 |  update(*args, **kwds)
 |      D.update([E, ]**F) -> None.  Update D from mapping/iterable E and F.
 |      If E present and has a .keys() method, does:     for k in E: D[k] = E[k]
 |      If E present and lacks .keys() method, does:     for (k, v) in E: D[k] = v
 |      In either case, this is followed by: for k, v in F.items(): D[k] = v
 |  
 |  ----------------------------------------------------------------------
 |  Data and other attributes defined here:
 |  
 |  __abstractmethods__ = frozenset({'__delitem__', '__getitem__', '__iter...
 |  
 |  ----------------------------------------------------------------------
 |  Methods inherited from Mapping:
 |  
 |  __contains__(self, key)
 |  
 |  __eq__(self, other)
 |      Return self==value.
 |  
 |  __getitem__(self, key)
 |  
 |  get(self, key, default=None)
 |      D.get(k[,d]) -> D[k] if k in D, else d.  d defaults to None.
 |  
 |  items(self)
 |      D.items() -> a set-like object providing a view on D's items
 |  
 |  keys(self)
 |      D.keys() -> a set-like object providing a view on D's keys
 |  
 |  values(self)
 |      D.values() -> an object providing a view on D's values
 |  
 |  ----------------------------------------------------------------------
 |  Data and other attributes inherited from Mapping:
 |  
 |  __hash__ = None
 |  
 |  __reversed__ = None
 |  
 |  ----------------------------------------------------------------------
 |  Class methods inherited from Collection:
 |  
 |  __subclasshook__(C) from abc.ABCMeta
 |      Abstract classes can override this to customize issubclass().
 |      
 |      This is invoked early on by abc.ABCMeta.__subclasscheck__().
 |      It should return True, False or NotImplemented.  If it returns
 |      NotImplemented, the normal algorithm is used.  Otherwise, it
 |      overrides the normal algorithm (and the outcome is cached).
 |  
 |  ----------------------------------------------------------------------
 |  Methods inherited from Sized:
 |  
 |  __len__(self)
 |  
 |  ----------------------------------------------------------------------
 |  Methods inherited from Iterable:
 |  
 |  __iter__(self)

Python 里最常见的映射就是 dict 了,这种内置类型由 C 实现,性能很高,而且封装了很多实用操作。简单举例

  • 清空字典--clear
In [4]:
a = {'a':'A','b':'B'}
a.clear()
a
Out[4]:
{}
  • 获取字典存储内容--items
In [5]:
obj_dict = {'a':'A','b':'B'}
for key, val in obj_dict.items():
    print(key, val)
a A
b B
  • 从可迭代对象中生成字典--fromkeys
In [8]:
lst_key = ['a','b']
same_val = ['A','B']

obj_dict = dict.fromkeys(lst_key,same_val)
obj_dict
Out[8]:
{'a': ['A', 'B'], 'b': ['A', 'B']}
  • 浅拷贝--copy(深拷贝要靠 copy.deepcopy)
In [9]:
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)
{'a': ['C', 'B'], 'b': ['C', 'B']} {'a': ['C', 'B'], 'b': ['C', 'B']}
{'a': ['C', 'B'], 'b': ['C', 'B']} {'a': ['A', 'B'], 'b': ['A', 'B']}
  • 查询--get(没找到会返回空字典)
In [10]:
obj_dict.get('c')
In [11]:
obj_dict['c']
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
<ipython-input-11-c46bb2eb8d35> in <module>
----> 1 obj_dict['c']

KeyError: 'c'
  • 设置或添加默认值--setdefault
In [12]:
obj_dict.setdefault('c','C')
obj_dict
Out[12]:
{'a': ['C', 'B'], 'b': ['C', 'B'], 'c': 'C'}
In [13]:
obj_dict.setdefault('a','A')
obj_dict
Out[13]:
{'a': ['C', 'B'], 'b': ['C', 'B'], 'c': 'C'}
  • 追加/修改元素--update
In [15]:
obj_dict.update((('a','A'),))
obj_dict
Out[15]:
{'a': 'A', 'b': ['C', 'B'], 'c': 'C'}
In [20]:
obj_dict.update({'d':'D'})
obj_dict
Out[20]:
{'a': 'A', 'b': ['C', 'B'], 'c': 'C', 'd': 'D'}
In [21]:
obj_dict.update(e='E',f='F')
obj_dict
Out[21]:
{'a': 'A', 'b': ['C', 'B'], 'c': 'C', 'd': 'D', 'e': 'E', 'f': 'F'}

如果想要自己封装一个字典,不建议继承 dictlist 同理),子类的覆盖方法有时不会生效

In [25]:
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)
{'one': 1}
{'one': 1, 'two': 4}

比较好的继承对象是 collections.Userdict,但更推荐通过实例化 collections.defaultdict 来构建自己的字典,在其内部__getitem__ 的实现中通过重写 __missing__ 方法,能做到当查询键不存在于字典中时,不报异常而是走缺失处理程序

In [28]:
from collections import defaultdict

my_dict = defaultdict(dict)

my_dict['key']
Out[28]:
{}

常和字典一起说的是集合 set,Python 里的集合能执行集合运算。此外还有一个 frozenset,顾名思义 frozenset 的值初始化后就不可更改了,所以很适合做字典的 key

In [29]:
f_set = frozenset('abc')
n_set = set('abc')
print(n_set, f_set)
{'c', 'b', 'a'} frozenset({'c', 'b', 'a'})

而如果深究实现机制,集合就是字典的特例,因为字典中对键 key 的要求完全符合集合的特点,所以集合可以用所有 key 的 val 都为空的字典封装实现。

字典的实现一般是基于哈希法,查询时先对键求散列值,定位到散列表的一个表元,如果此时表元为空则进入异常处理流程。如果表元有值,因为存在不同元素散列值相同的情形,需要确认 key 是否相同,不同则进入哈希冲突处理流程,比如再散列,链地址等,相同则正常返回。Python 内置 hash() 方法获取对象哈希值,也可以自己实现__hash__方法来自定义哈希规则

哈希函数因为是通过索引直接访问表元,故读取效率极高,只有$O(1)$的时间复杂度,但内存占用也会大些。与之相比,list 为代表的序列类型往往效率会低很多。Python 对自己的内部对象和用户自定义对象都用 dict 进行了封装。

同时为了确保唯一性,参与哈希计算的元素必须不可变,也就导致 key 只能是 str tuple 这类不可变对象。

可变性

说对象肯定得提一提变量。Java,C# 中的变量申明时会指定类型,初始化时计算机去内存开辟一块空间打上类型标识,从此变量就和这块空间绑定在了一起。Python 作为动态语言,变量和对象间的关系没有这么亲密,Python 变量实质只是一个指针,对象初始化完毕后,使变量指针指向对象的内存空间,所以 Python 变量更像是对象的别名

In [1]:
one_lst = [1,2,3]
two_lst = one_lst
two_lst.append(4)  # 指针指向位置相同
one_lst
Out[1]:
[1, 2, 3, 4]

变量行为上,我们曾经提到过 ==is 的区别,前者实质是调用 __eq__ 方法进行值比对,is 则是比较两变量指向的内存空间是否一致。Python 在这种普遍规律上做一定的优化,如果是小整数(-5~256),短字符串,每次声明新变量时,Python 不会构建新对象而是会寻找上一个值相同的对象然后返回。

In [4]:
num_obj = 2
another_num_obj = 2
print(id(num_obj), id(another_num_obj))  # 小整数指向相同
140715413836640 140715413836640
In [21]:
str_obj = 'abc'
another_str_obj = 'abc'
print(id(str_obj),id(another_str_obj))
2224771828848 2224771828848

有创建就有删除,Python 提供了 del 方法来删除变量。实质上,因为变量只是对象别名的缘故,Python 采用的是引用计数的方法来判断是否释放空间,执行del(var_name)时会将var_name指针释放,指针指向对象内部维护的引用计数减一,引用计数为 0 时释放对象占据的内存空间。

对于自定义的对象,可以通过配置__del__方法告知解释器在回收该对象时要做些什么。

In [22]:
a = 'abc'
b = a
del a  # 释放指针,引用计数减一

b  # 计数不为零依旧可访问
Out[22]:
'abc'

可变性除了变量自身,对象本身的可变性有时也会给人带来困扰。比如下面这个例子

In [24]:
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)
[1, 2, 3, 4] [1, 2, 3, 4] [3, 4]

这里定义了一个加法函数 add,执行两个加数的求和运算。传入了两个列表参数后,其中一个加数param1也被改变了。

造成这一问题的原因是 list 本身就是一种可变对象,传递参数时将自己的引用传入,而 += 本身是一种就地变换,导致 param1 实际既是参数又是结果。

所以编程实践中不要使用可变对象作为参数,也不要令默认参数为可变对象