collections:内建模块,提供额外的集合类

楔子

collections 是一个很有用的内置模块,除了帮我们轻松地完成某些操作之外,里面还包含了很多除了内置类型之外的数据类型。

搜索多个字典

collections 里面的 ChainMap 对象类似于一个字典序列,可以按照内部字典出现的顺序依次查找和 key 关联的 value。可以把 ChainMap 对象看成一个栈,栈增长时发生变更,栈收缩时这些变更将被丢弃。

from collections import ChainMap

d1 = {"a": 12, "b": 22, "c": 33}
d2 = {"b": 1, "c": 2, "d": 3}

# 将多个字典传进去
d = ChainMap(d1, d2)
for k, v in d.items():
    print(k, v)
    '''
    b 22
    c 33
    d 3
    a 12
    '''
    # 可以看到打印的结果是无序的,而且如果多个字典中有相同的key,那么只保留第一次出现的key

# 并且 ChainMap 对象有一个 maps 属性,存储了要搜索的映射列表。这个列表是可变的。所以可以直接增加新映射,或者改变元素的顺序以控制查找和更新行为。
print(d.maps)  # [{'a': 12, 'b': 22, 'c': 33}, {'b': 1, 'c': 2, 'd': 3}]

# 这是我们存储的信息,如果在d.maps里面修改了,那么会怎么样呢?
print(d1)  # {'a': 12, 'b': 22, 'c': 33}
d.maps[0]["a"] = "yoyoyo"
# 可以看到d.maps里面存储的只是一个引用,因此改变之后会影响原来的结果
print(d1)  # {'a': 'yoyoyo', 'b': 22, 'c': 33}

# 那我如果改变了原来的值,会不会影响d.maps呢?显然是可以的,毕竟同一个内存地址嘛
d2["d"] = "我屮艸芔茻"
print(d.maps)  # [{'a': 'yoyoyo', 'b': 22, 'c': 33}, {'b': 1, 'c': 2, 'd': '我屮艸芔茻'}]

统计可散列的对象

collections 里面的 Counter 可以用来进行统计,个人觉得这是该模块里面最方便的功能了,下面来看看用法。

from collections import Counter
 
 
# Counter 接收一个序列,可以计算出序列中每一个元素出现的次数
print(Counter("aabbbc"))  # Counter({'b': 3, 'a': 2, 'c': 1})
print(Counter(['a', 'a', 'b', 'b', 'b', 'c']))  # Counter({'b': 3, 'a': 2, 'c': 1})
print(Counter(a=2, b=3, c=1))  # Counter({'b': 3, 'a': 2, 'c': 1})

当然我们还可以对 Counter 对象进行更新

from collections import Counter

c = Counter("aaabbc")
# 表示a出现了三次,b出现了两次,c出现了一次
print(c)  # Counter({'a': 3, 'b': 2, 'c': 1})
# 可以进行填充
c.update("bcd")
# 可以看到b和c的值都增加了1,并且出现了d
print(c)  # Counter({'a': 3, 'b': 3, 'c': 2, 'd': 1})

# 访问计数,Counter对象可以像字典一样访问
print(c["a"])  # 3
# 如果访问一个不存在的key,不会引发KeyError,而是会返回0,表示对象中没有这个key
print(c["mmp"])  # 0
 
# 还可以使用elements进行迭代,会得到Counter对象中的所有元素
print(list(c.elements()))  # ['a', 'a', 'a', 'b', 'b', 'b', 'c', 'c', 'd']

# 还可以计算出现最多的元素
# 统计 string 中前三个出现次数最多的元素
string = "sasaxzsdsadfscxzcasdscxzdfscxsasadszczxczxcsds"
c = Counter(string)
print(c)  # Counter({'s': 13, 'c': 7, 'a': 6, 'x': 6, 'z': 6, 'd': 6, 'f': 2})
print(c.most_common(3))  # [('s', 13), ('c', 7), ('a', 6)]

Counter 还没有这么简单,它还可以实现算数操作,以及位运算。

from collections import Counter

c1 = Counter("aabbccc")
c2 = Counter("bbbccdd")
print(c1)  # Counter({'a': 2, 'b': 2, 'c': 3})
print(c2)  # Counter({'b': 3, 'c': 2, 'd': 2})
# 如果c1的元素出现在了c2中,就把该元素减去,记住:减的是次数
print(c1 - c2)  # Counter({'a': 2, 'c': 1})
'''
a在c1中出现了2次,c2中没有出现,所有是a: 2。b在c1中出现两次,在c2中出现3次,所以一减就没有了。
而c在c1中出现了三次,在c2中出现两次,所以相减还剩下一次。至于c1没有的元素就不用管了
'''
 
# 相加就很好理解了
print(c1 + c2)  # Counter({'b': 5, 'c': 5, 'a': 2, 'd': 2})
 
# 相交的话,查找公共的元素,并且取次数出现较小的那个
print(c1 & c2)  # Counter({'b': 2, 'c': 2})
 
# 并集的话,取较大的,记住不是相加,所以b和c出现的次数不会增加,只是取较大的那个、
print(c1 | c2)  # Counter({'b': 3, 'c': 3, 'a': 2, 'd': 2})

缺少的键返回一个默认值

很明显,这是针对于字典的。首先 Python 中的字典也支持这种操作,有setdefault和get,可以用来获取key对应的value,并且还能在 key 不存在的时候给一个默认值。如果 key 存在,两者会获取 key 对应的 value;但如果 key 不存在,setdefault 就会先将 key 和指定的默认值设置进去,然后再将设置的值返回,而 get 则只会返回默认值,不会进行设置。举个栗子:

d = {"a": 1}
print(d.get("a", 0))  # 1
print(d.setdefault("a", 0))  # 1
print(d)  # {"a": 1}

print(d.get("b", 0))  # 0
print(d)  # {"a": 1}

print(d.setdefault("b", 0))  # 0
print(d)  # {"a": 1, "b": 0}
# 所以这里相当于执行了两步操作。先将 "b": 0 设置到字典里,然后再获取

而 collections 中的 defaultdict在初始化的时候就会让调用者提前指定类型,当获取不到指定 key 的时候就会返回默认类型对应的零值。

from collections import defaultdict


d2 = defaultdict(int)
print(d2["a"])  # 0
d2 = defaultdict(str)
print("%r" % d2["a"])  # ''
d2 = defaultdict(tuple)
print(d2["a"])  # ()
d2 = defaultdict(list)
print(d2["a"])  # []
# 如果获取不到key,那么会自动输出传入类型所对应的零值.能获取到key,输入key对应的value值

比如我们来统计元素出现的次数。

# 统计每一个元素出现的次数 
s = "aabbccdddddee"
d1 = {}
for c in s:
    # 不存在就默认设置为0
    d1.setdefault(c, 0)
    d1[c] += 1
print(d1)  # {'a': 2, 'b': 2, 'c': 2, 'd': 5, 'e': 2}


from collections import defaultdict

s = "aabbccdddddee"
d2 = defaultdict(int)
for c in s:
    '''
    一开始没有值,设置为0,然后每来一个值就加上1
    '''
    d2[c] += 1
print(d2)  # defaultdict(<class 'int'>, {'a': 2, 'b': 2, 'c': 2, 'd': 5, 'e': 2})

怎么样,是不是很方便呢?在实例化 defaultdict 的时候,指定一个类型即可,获取一个不存在的 key 的时候,会返回指定的类型的零值。

此外还可以自定义返回值,只需要添加一个不需要参数的函数即可,并指定一个返回值。

from collections import defaultdict

# 此时的默认值就是default
d3 = defaultdict(lambda: "default")
print(d3["aa"])  # default

# 此外还可以添加参数,因为单独指定了aa,所以打印的时候以指定的为准
# 如果没有指定,那么才会得到默认值
d4 = defaultdict(lambda: "default", aa="bar")
print(d4["aa"])  # bar
print(d4["bb"])  # default

那么肯定会有人好奇,这是如何实现的呢?其实里面主要使用了一个叫做 __missing__ 的魔法方法,字典在查找元素的时候,会使用 __getitem__,然后在找不到的时候会去找 __missing__。但是注意:dict 这个类本身并没有实现 __missing__,所以我们需要继承自 dict,然后在子类中实现。

class MyDict(dict):

    def __getitem__(self, item):
        value = super().__getitem__(item)
        # 会执行父类的 __getitem__ 方法,如果获取不到
        # 会检测我们是否定义 __missing__ 方法,如果有,执行。没有,报错
        # 所以这里的 value 就是 __missing__ 方法的返回值
        return value

    def __missing__(self, key):
        self[key] = "搞事情ヘ(´ー`ヘ)搞事情"
        return self[key]


d = MyDict([("a", 3), ("b", 4)])
print(d)  # {'a': 3, 'b': 4}
print(d["mmm"])  # 搞事情ヘ(´ー`ヘ)搞事情

双端队列

双端队列支持从任意一端增加和删除元素。更为常用的两种数据结构(即栈和队列)就是双端队列的退化形式,它们的输入和输出被限制在某一端。

from collections import deque
 
 
d = deque("abcdefg")
print(d)  # deque(['a', 'b', 'c', 'd', 'e', 'f', 'g'])
print(len(d))  # 7
print(d[0])  # a
print(d[-1])  # g
 
# 由于deque是一种序列容器,因此同样支持list的操作。如:通过索引获取元素,查看长度,删除元素,反转元素等等
# list支持的deque基本上都支持
d.reverse()
print(d)  # deque(['g', 'f', 'e', 'd', 'c', 'b', 'a'])
d.remove("c")
print(d)  # deque(['g', 'f', 'e', 'd', 'b', 'a'])

除了增删改查,并且还支持其他操作。

from collections import deque

d = deque("abcdefg")

# 填充元素
# 首先可以像list一样添加元素,但是deque可以从两端添加
d.append("yoyoyo")  # 默认和list一样,在尾部添加
d.appendleft("哟哟哟")  # 也可以添加在头部
print(d)  # deque(['哟哟哟', 'g', 'f', 'e', 'd', 'b', 'a', 'yoyoyo'])
# 还可以使用insert, 如果范围越界,自动添加在两端
d.insert(100, "x")
print(d)  # deque(['哟哟哟', 'g', 'f', 'e', 'd', 'b', 'a', 'yoyoyo', 'x'])

# extend,extendleft
d1 = [1, 2, 3]
d2 = deque([4, 5, 6])
d.extend(d1)
print(d)  # deque(['哟哟哟', 'g', 'f', 'e', 'd', 'b', 'a', 'yoyoyo', 'x', 1, 2, 3])
d.extendleft(d2)
print(d)  # deque([6, 5, 4, '哟哟哟', 'g', 'f', 'e', 'd', 'b', 'a', 'yoyoyo', 'x', 1, 2, 3])
# 可以看到extend也支持从左端添加,而且不仅仅可以添加deque,任意序列类型都是可以的。
d.extendleft("我屮艸芔茻")
print(d)  # deque(['茻', '芔', '艸', '屮', '我', 6, 5, 4, '哟哟哟', 'g', 'f', 'e', 'd', 'b', 'a', 'yoyoyo', 'x', 1, 2, 3])
# 注意添加的顺序,我们是从左边开始添加的,先添加 我",然后 "屮" 跑到开头就把 "我" ,所以是结果是倒过来的


# 那么如果消费deque里面的元素呢?
print(d.pop())  # 3
print(d.pop())  # 2
print(d.pop())  # 1
print(d.pop())  # x
print(d.popleft())  # 茻
# pop是从右端删除一个元素,popleft是从左端开始删除一个元素。但是如果我想pop掉指定的索引的元素,只能用pop函数,传入索引值即可
# 注意:deque和queue一样,是线程安全的,是受GIL这把超级大锁保护的,可以不同的线程中进行消费。
# 如果想清空里面的元素的话,可以像list、dict一样,使用clear函数
d.clear()
print(d)  # deque([])

# 旋转
# deque还有一个很用的地方就是可以按任意一个方向进行旋转,从而跳过某些元素。
# d.rotate(n)-->n大于0,从右边开始取n个元素放到左边,n小于0,从左边取n个元素放到右边
d = deque(range(10))
print(d)  # deque([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
d.rotate(2)
# 从右边取2个元素放到左边,所以8和9被放到了左边
print(d)  # deque([8, 9, 0, 1, 2, 3, 4, 5, 6, 7])
d.rotate(-3)
# 从左边取3个元素放到右边,所以8、9、0被放到了右边
print(d)  # deque([1, 2, 3, 4, 5, 6, 7, 8, 9, 0])

当然双端队列默认是容量无限的,但现实情况肯定是需要给队列加上容量限制的,如何加呢?

# 限制队列的大小
# 我们在初始化一个双端队列的时候,还可以限制它的大小
d = deque("abcdefg", maxlen=5)
# 我们初始化7个元素,但是指定最大长度只有5,所以前面两个("a"和"b")就被挤出去了
print(d)  # deque(['c', 'd', 'e', 'f', 'g'], maxlen=5)
d.appendleft("yoyoyo")
# 当我往前面添加元素的时候,后面的就被挤出去了,因为队列最多只能容纳5个元素
print(d)  # deque(['yoyoyo', 'c', 'd', 'e', 'f'], maxlen=5)

带名字字段的元组子类

元组的话,我们都是通过索引来获取元素,但是通过索引的话,如果你不手动数一数的话,你是不知道该索引要获取哪一个元素的。所以问题来了,可不可以给里面的元素一个字段名呢?我们通过字段名来获取对应的值不就行啦,没错,这就是 namedtuple。

from collections import namedtuple
 
 
# 传入名字,和字段
person = namedtuple("person", ["name", "age", "gender"])
person1 = person(name="mashiro", age=16, gender="f")
print(person1)  # person(name='mashiro', age=16, gender='f')
print(person1.name, person1.age, person1.gender)  # mashiro 16 f
print(person1[0])  # mashiro
'''
可以看到不仅可以像普通的tuple一样使用索引访问,还可以使用像类一样通过.字段名访问
'''
 
person2 = person("satori", 16, "f")
print(person2)  # person(name='satori', age=16, gender='f')
 
'''
注意:这个和普通的元组一样,是不可以修改的
'''
try:
    person2.name = "xxx"
except AttributeError as e:
    print(e)  # can't set attribute
 
 
# 非法字段名,不能使用Python的关键字
try:
    girl = namedtuple("女孩们", ["for", "in"])
except ValueError as e:
    print(e)  # Type names and field names cannot be a keyword: 'for'
 
 
# 如果字段名重复了怎么办
try:
    girl = namedtuple("女孩们", ["name", "age", "age"])
except ValueError as e:
    print(e)  # Encountered duplicate field name: 'age'
 
# 如果非要加上重名字段呢,可以设置一个参数
girl = namedtuple("女孩们", ["name", "age", "age"], rename=True)
print(girl)  # <class '__main__.女孩们'>
girl1 = girl("koishi", 15, 15)
# 可以看到重复的字段名会按照索引的值,在前面加上一个下划线。比如第二个age重复,它的索引是多少呢?是2,所以默认帮我们把字段名修改为_2
print(girl1)  # 女孩们(name='koishi', age=15, _2=15)
 
# 此外我们所有的字段名都保存在 _fields 属性中
print(girl1._fields)  # ('name', 'age', '_2')

记住键值对顺序的字典

OrderDict 是一个字典子类,可以记住字典中增加键的顺序。在Python2中,字典是无序的,但在Python3中,字典默认是有序的,所以这个个人觉得不是很常用,至少在 Python3 中感觉不是很常用。

from collections import OrderedDict
 

d = OrderedDict()
d["a"] = "A"
d["b"] = "B"
d["c"] = "C"
for k, v in d.items():
    print(k, v)
'''
a A
b B
c C
'''
# 此外也可以在初始化的时候,添加元素
print(OrderedDict({"a": 1}))  # OrderedDict([('a', 1)])
 
 
# 相等性,对于常规字典来说,只要里面元素一样便是相等的,不考虑顺序。但是对于OrderDict来说,除了元素,顺序也要一样,否则就不相等
d1 = {"a": 1, "b": 2}
d2 = {"b": 2, "a": 1}
print(d1 == d2)  # True
 
d1 = OrderedDict({"a": 1, "b": 2})
d2 = OrderedDict({"b": 2, "a": 1})
print(d1 == d2)  # False
 
 
# 重排
# 在OrderDict中可以使用 move_to_end() 将键移至序列的起始位置或末尾位置来改变键的顺序
d3 = OrderedDict({"a": 1, "b": 2, "c": 3, "d": 4})
d3.move_to_end("c")  # 表示将 key="c" 的这个键值对移动到末尾
print(d3)  # OrderedDict([('a', 1), ('b', 2), ('d', 4), ('c', 3)])
d3.move_to_end("c", last=False)  # 表示将 key="c" 的这个键值对移动到行首
print(d3)  # OrderedDict([('c', 3), ('a', 1), ('b', 2), ('d', 4)])

小结

collections 这个模块个人觉得用处还是挺大的,专门用来处理容器的,特别是 Counter,在统计数量方面真的很方便。

posted @ 2019-11-14 23:02  古明地盆  阅读(303)  评论(0编辑  收藏  举报