python的成功除了语言本身的设计非常受喜欢外,还有一个更大的原因是其开源库的繁荣,尤其是机器学习相关的库,但除此以外,python官方也开发了很多标准库,这些库有些是为了给我们提供原生语言不支持的功能,而有的是为了给我们提供一些高频函数和类的实现
今天要介绍的是collections库,从英文名就能看出来其是有关容器的库,这个库从底层给我们提供了一些原生python中很难甚至是不可能实现的容器,也用原生python写了一些常见容器
try:
from _collections import deque
except ImportError:
pass
else:
_collections_abc.MutableSequence.register(deque)
try:
from _collections import defaultdict
except ImportError:
pass
我们可以在collections库的开头看到这几行代码,deque和defaultdict就是从python底层实现的新容器,也就是用C语言写的,今天主要介绍它们俩及常用场景
from collections import deque
# deque可以接受一个可迭代对象,并生成对应的deque容器
dq = deque(i for i in range(10))
上述代码是通过生成器表达式创建了一个0-9的deque容器,deque翻译过来叫双端队列,它有内置类型list的append和pop方法,但仔细观察,我们可以发现,deque的pop方法没有可传参数,也就是说deque的pop只可以弹出尾端的元素,deque还有appendleft和popleft两个方法,表示在左侧增加和删除元素
刚接触python的人会认为deque是list的一个子集,list也可以通过pop来删除第一个元素,通过insert来插入元素到表头,但实际上可以说两者基本没什么关系,deque是官方提供的高效队列实现,是对容器的扩展,为什么说是高效呢?
from time import time
from collections import deque
T = 10 ** 5
dq = deque(i for i in range(T))
ls = list(i for i in range(T))
s = time()
for _ in range(T):
dq.popleft() # 0.00398707389831543
e1 = time()
print(e1 - s)
for _ in range(T):
ls.pop(0)
print(time() - e1) # 9.261945247650146
可以看到,list.pop(0)来模拟deque的popleft效率会低很多,这是因为list的pop(0)时间复杂度实际上是O(n),python会重新拷贝一遍这个list,而deque不会,因此为了模拟队列先进先出的特性,使用deque远远比list好
有人可能非要根据index来删除deque的某个元素,实际上这也是可以的,使用关键字del即可
from collections import deque
dq = deque(i for i in range(10))
del dq[5] # 删除index==5的元素
print(dq) # deque([0, 1, 2, 3, 4, 6, 7, 8, 9])
deque还有一个list没有的方法rotate,就是将元素向右移动,传入负数可以向左移动,其实该方法的实现也非常简单,感兴趣可以自己实现一下
from collections import deque
dq = deque(i for i in range(10))
dq.rotate(3)
print(dq) # deque([7, 8, 9, 0, 1, 2, 3, 4, 5, 6])
dq.rotate(-5)
print(dq) # deque([2, 3, 4, 5, 6, 7, 8, 9, 0, 1])
另一个C实现的容器时defaultdict,翻译过来叫默认值字典,其接受一个工厂函数,还有可选的第二个参数,但这个参数如果要使用需要熟悉泛型编程,没错,python也支持泛型编程,什么是泛型?泛型就是在API的传参和返回过程中,不规定特定类型,而是根据用户的输入动态地改变
from typing import TypeVar
_T = TypeVar('_T')
def add(x: _T, y: _T) -> _T:
return x + y
print(add(1, 2)) # 3
print(add("a", "b")) # ab
print(add([1, 2], [3, 4])) # [1,2,3,4]
print(add(1, 1.1)) # IDE会标注类型错误,但可以正常输出2.1
TypeVar就是泛型,该add函数接受一个x和y,要求x和y必须是同类型的,并且返回同类型的值,但具体x和y是int还是str,并不关心,它们相同即可,否则IDE会提醒用户类型错误,但是注意,即使用户不遵守,只要这两个类型实际上可以正确相加,那解释器不会报错,而是正常执行,这是因为python的type hint对实际运行没有任何影响,只是提高程序的可读性和可维护性的工具
from collections import defaultdict
dc1 = defaultdict(list)
dc1['a'].append('a') # 会创建一个新的键值对,'a': ['a']
dc2 = dict()
dc2['a'].append('a') # KeyError: 'a'
defaultdict又叫永不报KeyError的dict,在执行defaultdict的__getitem__方法时,若不存在该键,触发KeyError后,会根据工厂函数创建一个新的键值对,dc1那行代码可以成立也说明这个创建的值是会立即被返回的,然后我们就可以向自动创建的空list中添加'a'了
from collections import defaultdict
dc1 = defaultdict(list, {'a': [1, 2, 3]})
dc1['a'].append('a')
print(dc1) # defaultdict(<class 'list'>, {'a': [1, 2, 3, 'a']})
dc1 = defaultdict(list, {'a': 1}) # IDE会标注类型错误
print(dc1['a']) # 1
我们可以很清晰地知道第二个参数是干什么的了,就是指定在某个key下,默认按照该参数指定的实例来创建,那为什么说涉及到泛型呢,因为在这里第二个参数的value必须是工厂函数的实例,否则会产生意想不到的行为,但还是那句话,你如果实在不遵守,只要你的操作实际上不会出错,那么就不会出错
最后再说一下什么是工厂函数,工厂函数简单来说就是生产实例的函数,list就是工厂函数,因为在调用list()的时候,实际上返回了list的实例,因此任何类本身严格来说都是工厂函数,本质上真正的工厂函数是类中的__new__魔术方法,它真实地创建了这个实例
class H:
def __new__(cls, *args, **kwargs):
# 重写该方法,让它返回None
return
h = H() # 并不会创建H的实例
print(h) # None