Python自悟:Python中的Dict and Set

主要内容源自解读《Fluent Python》,理解如有错误敬请指正:-)

  • dict对象的最原始的接口描述是 collections 模块中的 Mapping 和 MutableMapping 这两个虚拟类,如下所示:


但是自定义的mapping类却大多继承 dict 或者 collections.UserDict 类来实现。不过通常可以使用 isinstance(mapObj, collections.Mapping) 而不是 isinstance(mapObj, dict) 这样的方式来更广义地判断一个对象是不是一个符合标准map接口的对象

  • Python官方文档中对于一个对象是否可哈希(Hashable),定义为

An object is hashable if it has a hash value which never changes during its lifetime (it needs a hash()
method), and can be compared to other objects (it needs an eq()
or cmp()
method). Hashable objects which compare equal must have the same hash value.

核心就是该对象必须要实现 __hash()__ 方法 和 __eq__() 方法,前者就是计算该对象的hash值,是内建函数 hash(obj) 的实际工作方法,可哈希的对象的哈希值必须是恒久不变的,并且__eq__()判断为相等的两个对象的哈希值也必须相同。
Python中内建的Hashable对象包括:str、bytes、数字类型、frozenset等immutable对象,所有的可变序列对象都不是可哈希的,对于tuple则仅当其中所有的元素都是Hashable对象时才是可哈希的
用户自定义的对象通常是可哈希的,这是因为这些对象的 __hash__() 方法默认返回的是该对象的id(即id(obj)返回的值),而一个对象的id是唯一的

>>> hash((1,2,3,4))
485696759010151909
>>> hash((1,2,[3,4]))
Traceback (most recent call last):
  File "<pyshell#21>", line 1, in <module>
    hash((1,2,[3,4]))
TypeError: unhashable type: 'list'
>>> hash((1,2,frozenset([3, 4])))
-4138728974339688815
>>> class MyObj:
    pass
>>> hash(MyObj())
273870000
>>> hash(MyObj())
273869928

—— Python标准库中的所有mapping类型对象,都满足其所有的key必须为可哈希对象的条件,这样也才能够保证mapping对象中key的唯一性

  • dict对象常用的几种定义方式如下:
>>> 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([('two', 2), ('one', 1), ('three', 3)])   # 元素类型为(key, value)的list、tuple等iterable对象都可以直接进行dict化
>>> e = dict({'three': 3, 'one': 1, 'two': 2})
>>> a == b == c == d == e
True

除此以外,还可以使用类似于 listcomps 的推导表示方法,基于元素类型为(key, value)的list、tuple等iterable对象,外层使用 大括号对{}来推导生成dict对象,即所谓的“dict comprehensions”

>>> areaCodeList = [
("Beijing", "010"), ("Guangzhou", "020"), ("Shanghai", "021"), ("Tianjin", "022"), ("Chongqing","023"), 
("Shenyang", "024"), ("Nanjing", "025"), ("Wuhan", "027"), ("Chengdu", "028"), ("Xian", "029")]

  >>> { code : city for city,code in areaCodeList}
{'025': 'Nanjing', '010': 'Beijing', '024': 'Shenyang', '027': 'Wuhan', '021': 'Shanghai', '020': 'Guangzhou', 
'023': 'Chongqing', '022': 'Tianjin', '029': 'Xian', '028': 'Chengdu'}

  >>> { code : city for city,code in areaCodeList if code.startswith("02")}
{'025': 'Nanjing', '024': 'Shenyang', '027': 'Wuhan', '021': 'Shanghai', '020': 'Guangzhou', '023': 'Chongqing', 
'022': 'Tianjin', '029': 'Xian', '028': 'Chengdu'}
  • dict、defaultdict、OrderedDict三种常用的mapping对象的主要方法和比较如下所示:


  • 所有的mapping类型对象都可以通过 update( ) 函数,传入新的mapping,或(key, value)作为元素的iterable对象,或者**kwargs参数进行内容的升级:

>>> dic1 = {"Beijing":"error"}; dic1
{'Beijing': 'error'}

  >>> dic1.update(areaCodeList, Shenzhen="0755", Hangzhou="0571");  dic1
{'Beijing': '010', 'Shanghai': '021', 'Chengdu': '028', 'Tianjin': '022', 'Guangzhou': '020', 'Xian': '029', 'Nanjing': '025', 
'Wuhan': '027', 'Hangzhou': '0571', 'Shenzhen': '0755', 'Shenyang': '024', 'Chongqing': '023'}
  • mapping对象的 mapObj.setdefault(key, [default]) 是一个值得特别注意的方法,我们不要被它的名字所迷惑而以为它是一个纯粹设置mapping对象中元素默认值的方法,它首先是一个读取指定key的value的方法,将会返回mapObj中的一个key的value,只是这个value将根据传入的key是否在当前的mapObj中而有所不同,如果不存在key将会在mapObj中加入 key:default 的元素,并返回default;如果key存在,则直接返回当前key对应的value值,而不会去修改当前key的值为default
>>> dic2 = {}
>>> dic2.setdefault("key1");  dic2
{'key1': None}
>>> dic2.setdefault("key2",[])
[]
>>> dic2
{'key2': [], 'key1': None}
>>> dic2.setdefault("key3","hello")
'hello'
>>> dic2
{'key3': 'hello', 'key2': [], 'key1': None}
>>> dic2.setdefault("key2")
[]
>>> dic2.setdefault("key3", "value3")  # 这一步并不会将key3对应的value进行修改
'hello'
>>> dic2
{'key3': 'hello', 'key2': [], 'key1': None}
>>> 

使用setdefault( )方法的优点主要是:
(1)、这个方法的调用不会出现因为key不存在而抛异常的情况,
(2)、对于需要先判断mapObj中是否包含某个key,然后再进行下一步操作的逻辑,使用这个方法将会使代码逻辑很简单,并且至少减少一次为了确认mapObj是否包含key而对整个mapObj对象key的遍历

  • mapping对象中对于 mapObj[key] 这个语法的支持本质上是调用 mapObj.__getitem__(key) 方法,但是当 __getitem__(key) 方法无法找到key的时候,mapObj对象的 __missing__(key) 方法就会被调用。
    (1). 对于dict对象,__missing__(key) 默认会抛出 KeyError 异常;
    (2). 对于 collections.defaultdict 对象,__missing__(key) 默认将会调用mapObj对象的default_factory属性,由default_factory来生成一个对象作为key的value——这一点功能类似于 setdefault( ) 方法
    特别需要注意的一点是,__missing__( ) 方法的调用仅仅会由 mapObj[key] 这个语法来触发,而 mapObj.get(key, default) 这个方法的调用是不会触发__missing__方法的
>>> class MyDict(dict):
          def __missing__(self, k):
               return "MissingValue of %s" % str(k) 
  
  >>> my_dict = MyDict()
>>> my_dict["key1"]
'MissingValue of key1'
>>> my_dict
{}
>>> my_dict.get("key1")   #  get(k[, default]) 不会触发 __missing__( ) 方法被调用
>>> my_dict.get("key1", 3)
3
  • collections.defaultdict 对象作用类似于 mapping 对象通用的 setdefault(key, default) 方法,可以避免因为key不存在而导致mapObj[key] 语法触发KeyError异常。二者的区别在于defaultdict对象是在初始化的时候指定default值产生的方式,而setdefault( ) 方法则是在被调用的时候来动态决定default值。
    defaultdict对象初始化时需要传入default_factory参数,这个参数是一个能够被调用的属性,既可以是一个有返回值的函数,也可以是一个类(被调用时即调用该类的初始化方法来返回一个对象)
>>> import collections, time
>>> defdict1 = collections.defaultdict(list)
>>> defdict1
defaultdict(<type 'list'>, {})
>>> defdict1['key1']
[]
>>> defdict1
defaultdict(<type 'list'>, {'key1': []})

  >>> def func():
          return "CurMilli: %d" % int(time.time() * 1000)
>>> defdict2 = collections.defaultdict(func)
>>> defdict2['key2']
'CurMilli: 1474045511728'
>>> defdict2
defaultdict(<function func at 0x105391ed8>, {'key2': 'CurMilli: 1474045511728'})
  • 当构造一个dict的时候,其中的 key:value 对并不是按照插入dict对象时的顺序来的。例如:
>>> areaCodeList
[('Beijing', '010'), ('Guangzhou', '020'), ('Shanghai', '021'), ('Tianjin', '022'), ('Chongqing', '023'), ('Shenyang', '024'), 
('Nanjing', '025'), ('Wuhan', '027'), ('Chengdu', '028'), ('Xian', '029')]

  >>> areaDict = {code : city for city, code in areaCodeList}

  >>> areaDict  # 其中的item的顺序和areaCodeList中的顺序并不一致
{'025': 'Nanjing', '010': 'Beijing', '024': 'Shenyang', '027': 'Wuhan', '021': 'Shanghai', '020': 'Guangzhou', 
'023': 'Chongqing', '022': 'Tianjin', '029': 'Xian', '028': 'Chengdu'}

如果需要保持这些 key:value 对插入的顺序,则需要使用 collections.OrderedDict 类:

>>> areaCodeList
[('Beijing', '010'), ('Guangzhou', '020'), ('Shanghai', '021'), ('Tianjin', '022'), ('Chongqing', '023'), ('Shenyang', '024'), 
('Nanjing', '025'), ('Wuhan', '027'), ('Chengdu', '028'), ('Xian', '029')]

  >>> sortedAreaDict = collections.OrderedDict([(code, city) for city, code in areaCodeList])
  >>> sortedAreaDict
OrderedDict([('010', 'Beijing'), ('020', 'Guangzhou'), ('021', 'Shanghai'), ('022', 'Tianjin'), 
('023', 'Chongqing'), ('024', 'Shenyang'), ('025', 'Nanjing'), ('027', 'Wuhan'), ('028', 'Chengdu'), ('029', 'Xian')])

  >>> sortedAreaDict.popitem()
('029', 'Xian')
>>> sortedAreaDict.popitem(last=False)
('010', 'Beijing')
  • collections.Counter 是一个很有意思的类,它是一个可用于统计Hashable对象数量的dict子类,其key就是待统计的Hashable对象,value就是这些对象的统计计数。Counter对象的 most_common(n) 方法可以很方便地统计出出现次数多的n个key
>>> import collections, random
>>> players =['Wilt Chamberlain', 'Michael Jordan', 'Kareem Abdul-Jabbar', 'Earvin Johnson', 'Kobe Bryant', 
'LeBron James', 'Stephen Curry', 'Bill Russell')

  >>> for i in range(100):
          votesForVIP += [random.choice(players)] * random.randrange(10,20)
>>> len(votesForVIP)
1433
>>> counter = collections.Counter(votesForVIP)
>>> counter
Counter({'Kobe Bryant': 233, 'Kareem Abdul-Jabbar': 210, 'Bill Russell': 196, 'Stephen Curry': 183, 
'Michael Jordan': 178, 'Earvin Johnson': 155, 'LeBron James': 139, 'Wilt Chamberlain': 139})
  
  >>> counter.most_common(3) 
[('Kobe Bryant', 233), ('Kareem Abdul-Jabbar', 210), ('Bill Russell', 196)]
  • 用户自定义类dict的class的时候,通常不要直接以dict作为父类,这是因为在覆盖重写dict类的 get(k, default)、__setitem__( )、__contain__( )、__missing__( ) 等方法时,常常又会使用到 mapObj[k]、 k in mapObj、mapObj[k] 等语法形式,这样一不小心就会造成这些内部方法的无穷递归调用。因此更建议使用 collections.UserDict 类而非dict来作为自定义mapping的父类

    collections.UserDict 名字中包含"dict",但是它并不是dict的子类,而是 collections.MutableMapping 的子类,因此UserDict类也继承了 __getitem__( )、__contain__( )、__setitem__( )等方法。UserDict类与dict的关联是通过UserDict对象中包含一个dict类型的成员变量 data 来实现的,data就作为真正的dict数据内容的保存地。用户自定义类dict class覆盖重写这些方法的时候,并不会递归调用UserDict类中其他的方法,而是对UserDict.data 变量进行相关操作,从而大大减轻了用户自定义类时对于死循环递归的防范难度,如下示例:

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):  # 对self.data操作不会导致自身的 __contains__ 函数的递归调用
        return str(key) in self.data   

    def __setitem__(self, key, item):  # 对self.data操作不会导致自身的 __setitem__ 函数的递归调用
        self.data[str(key)] = item  
  • set 和 frozenset 中每一个元素都是唯一而不重复的,这样的唯一性正是通过每个元素对象的hash值唯一来保证的,因此:
    (1)、set和frozenset中每一个元素对象必须是Hashable对象
    (2)、set本身不是Hashable对象(因为set是mutable对象)
    (3)、frozenset本身是immutable 对象,因此其本身也是Hashable对象

  • set和frozenset的定义方式包括:

>>> # 直接通过iterable对象来构建
>>> s = set([1,2,3,4,5,5,4,3,2,1,6,7,8,9,0,9,8,7,6])
>>> s
set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

  >>> # 对于所有元素都是直接常量值的set定义
>>> s = {1,2,3,4,5,6,5,4,3,2,1,0,9,8,7,6}
>>> s
set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
>>> # 但是定义空的set只能使用 set() 的方式,否则 {} 将会被当做是空的dict而非空的set
>>> empty_set = set(); type(empty_set)
<type 'set'>
>>> empty_dict = {}; type(empty_dict)
<type 'dict'>

  >>> # 使用 set comprehensions 推导方式,
>>> # 与dictcomps语法都是使用花括号{ },区别在于dictcomps使用 {key:value for ...} 形式而setcomps使用 {value for ... }形式
>>> from unicodedata import name
# 对于Python 2.7
>>> {unichr(i) for i in range(32,256) if 'SIGN' in name(unichr(i), "")}
set([u'#', u'\xa2', u'%', u'$', u'\xa7', u'\xb0', u'\xa9', u'+', u'\xf7', u'\xa3', u'\xac', u'\xb1', u'\xa4', u'\xb5', u'\xd7', 
u'\xb6', u'\xa5', u'\xae', u'=', u'<', u'>'])

  #对于Python 3
>>> {chr(i) for i in range(32,256) if 'SIGN' in name(chr(i), "")}
{'©', '<', '>', '¥', '¬', '§', '=', '#', '¢', '£', 'µ', '¶', '¤', '+', '®', '÷', '%', '°', '$', '×', '±'}

  # fronzenset只有一种定义方式:
>>> fs = frozenset([1,3,5,7,9])
  • set 在数学上对应着集合的概念,因此除了基本的加减运算之外,数学上的集合运算(and, or, xor, sub等)对于set对象也是完全支持的。
>>> set1 = {i for i in range(20)};  set1
set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19])
>>> set2 = {i for i in range(10) if i%2==0};  set2
set([0, 8, 2, 4, 6])
>>> set3 = {i for i in range(10) if i%2!=0};  set3
set([1, 3, 9, 5, 7])
>>> set4 = {i for i in range(20) if i%2==0}; set4
set([0, 2, 4, 6, 8, 10, 12, 14, 16, 18])
>>> set5 = {i for i in range(20) if i%2!=0}; set5
set([1, 3, 5, 7, 9, 11, 13, 15, 17, 19]) 
>>> 
>>> set1 & set2
set([0, 8, 2, 4, 6])
>>> set1 ^ set2  # set1和set2中各自包含但是对方不包含的元素的集合
set([1, 3, 5, 7, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19])
>>> set2 | set3
set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
>>>
>>> set1.intersection(set2, set4)  # set1.intersection(set2, set4, ...) 相当于 set1 & set2 & set4 &...
set([0, 8, 2, 4, 6])
>>> set1.intersection(set2, set3)
set([])
>>> set2.union(set3)
set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
>>> set2.union(set3, set4)  # set2.union(set3, set4, ...) 相当于 set2 | set3 | set4 | ...
set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12, 14, 16, 18])
>>>
>>> set1 - set2
set([1, 3, 5, 7, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19])
>>> set2 - set1   #  因为set2是set1的子集,所以set2 - set1的结果为空
set([])
>>> set1.difference(set2, set3)  #  set1.difference(set2, set3, ...) 相当于 set1 - set2 - set3 - ...
set([10, 11, 12, 13, 14, 15, 16, 17, 18, 19])
>>> set2.symmetric_difference(set1)  #  symmetric_difference( ) 函数就是亦或函数,等同于set2 ^ set1
set([1, 3, 5, 7, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19])
>>>
>>> set1.isdisjoint(set2)  #  isdisjoint( ) 函数等同于判断两个set的交集是否为空,即 len(set1 & set) == 0
False
>>> set2.isdisjoint(set3)
True
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容

  • 个人笔记,方便自己查阅使用 Py.LangSpec.Contents Refs Built-in Closure ...
    freenik阅读 67,682评论 0 5
  • The Python Data Model If you learned another object-orien...
    plutoese阅读 1,715评论 0 51
  • 有时候我们需要编写一些小的代码片段时,在Visual Studio中创建一个工程就显得有点杀鸡用牛刀的感觉了,所有...
    简约生活_忆沙阅读 1,726评论 0 0
  • 一、基本数据类型 注释 单行注释:// 区域注释:/* */ 文档注释:/** */ 数值 对于byte类型而言...
    龙猫小爷阅读 4,257评论 0 16
  • 1、memcache的概念? Memcache是一个高性能的分布式的内存对象缓存系统,通过在内存里维护一个统一的巨...
    桖辶殇阅读 2,230评论 2 12