【标准库】collections的OrderedDict和Counter

        上次提到了标准库collections中两个C语言实现的容器,由于本人对C语言并不算熟悉,因此无法解析源码,今天解析OrderedDict和Counter,当然它们也有C实现;

        首先看一段collections库的源码:

class _Link(object):
    __slots__ = 'prev', 'next', 'key', '__weakref__'

        这是一个链表,__slots__是类中的一个特殊属性,它应该是一个元组,如果一个类设置了该属性,那么这个类的实例只允许拥有元组中的实例属性

class Slots:
    __slots__ = ('name', 'age')


slots = Slots()
slots.name = 'hah'
slots.other = 'a'    # AttributeError: 'Slots' object has no attribute 'other'

        便于我们更好地理解发生了什么,我们可以观察以下代码的输出:1、__module__,当前的对象属于哪个模块,很显然就在当前的模块,也就是__main__;2、__doc__,当前对象的注解,没有添加注释,都是None;3、__slots__替代了__dict__并且给原对象添加了name和age两个member(这个member不是类属性),也就是说使用Slot1创建的实例,不再维护__dict__属性,也就无法动态地给其实例添加实例属性了,这在内存上是一个非常大的优化

class Slots1:
    __slots__ = ('name', 'age')
class Slots2:
    pass


# {'__module__': '__main__', '__slots__': ('name', 'age'), 'age': <member 'age' of 'Slots1' objects>, 'name': <member 'name' of 'Slots1' objects>, '__doc__': None}
print(Slots1.__dict__)
# {'__module__': '__main__', '__dict__': <attribute '__dict__' of 'Slots2' objects>, '__weakref__': <attribute '__weakref__' of 'Slots2' objects>, '__doc__': None}
print(Slots2.__dict__)

1、OrderedDict        

        先看源码中对OrderedDict的初始化,既然是标准库,那么对于性能方面是非常严苛的,这里用到了weakref库,跟垃圾回收相关,我们暂且不关心这个部分,我们只需重点关注其用到的数据结构,包括一个链表(有序,删除和添加效率极高,但查询效率为O(n))和哈希表(无序,查询、删除和添加效率极高,除非恶意造成哈希坍塌),有序无序是相对于能否保持插入的顺序而言的,当然,python3较高的版本中dict本身就是有序的;

    def __init__(self, other=(), /, **kwds):    # /表示在此之前的参数必须是位置参数
        try:
            self.__root
        except AttributeError:
            self.__hardroot = _Link()    # **链表
            self.__root = root = _proxy(self.__hardroot)    # 哨兵节点,是一个链表节点的弱引用
            root.prev = root.next = root    # root.prev是尾部的弱引用,root.next记录首部
            self.__map = {}    # **哈希表
        self.__update(other, **kwds)

        如果所有方法都看,那么实在是太多了,我们解析一个原生dict没有的方法,也是我们为什么在dict已经有序的情况下还有使用OrderedDict的需求:

    def move_to_end(self, key, last=True):
        # 移动键值为key的项到有序字典的尾部,last=False时变为移到首部
        link = self.__map[key]    # 通过哈希表快速查询到key对应的链表节点
        link_prev = link.prev
        link_next = link.next
        soft_link = link_next.prev    # soft_link是link的弱引用
        link_prev.next = link_next    # 这两步实际上在链表中删除了link
        link_next.prev = link_prev
        root = self.__root
        if last:
            last = root.prev    # 拿到尾部,这也是一个弱引用
            link.prev = last    # link的prev为last,也就是link作为last
            link.next = root    # link的next指向root
            root.prev = soft_link    # root的prev为link的弱引用
            last.next = link    # 不要忘记把last的next也设置为link,这样才是完整的双向链表
        else:
            first = root.next    # 拿到首部
            link.prev = root    # 后面与last的处理方式一样
            link.next = first
            first.prev = soft_link
            root.next = link

       link.next.prev和link的内存地址是不同的,这跟弱引用有关,python中的垃圾回收机制是引用计数,也就是一个对象不再被引用时,就会执行垃圾回收,但垃圾回收的时机不一定是立刻;我们可以阅读源码的__setitem__,在新增一个键值对的时候,root.prev会被赋值为一个link的弱引用,也就是说OrderedDict的链表结构实际上是用真实节点正向连接,弱引用反向连接

    def __setitem__(self, key, value,
                    dict_setitem=dict.__setitem__, proxy=_proxy, Link=_Link):
        if key not in self:
            self.__map[key] = link = Link()
            root = self.__root
            last = root.prev    # 这两行代码说明所有节点的prev都是weakproxy的实例
            link.prev, link.next, link.key = last, root, key
            last.next = link
            root.prev = proxy(link)    # root.prev是link的弱引用
        dict_setitem(self, key, value)

        为什么要这么做呢?答案是避免循环引用造成的内存泄漏(有一块内存空间在程序运行过程中不会再被释放)。由于在OrderedDict中的所有节点prev一定是一个弱引用,因此两个节点双向连接不会出现你中有我我中有你的循环引用情况,当其中一个节点被删除后,它将会被回收,但如果是强引用,删除其中一个后,另一个节点里面还引用了这个被删除的节点,它将不会被回收,这块内存就释放不了了;

        说了这么多,我们可以看到python标准库的代码质量是非常高的,接下来讲一下OrderedDict的实用场景,当然就是LRU了,LRU是一种缓存机制,最长时间未使用过的缓存对象会被优先删除,主要就需要使用到下述三个方法,具体写法就不再赘述了;

from collections import OrderedDict

od = OrderedDict()
od.popitem(last=False)  # 可以删除首部的元素,在超过指定最大容量是调用即可
od.move_to_end('recently used')  # 把最近使用过的元素移到尾部,不会被优先删除
od.__setitem__('new key', 'new value')  # 新增元素到尾部

2、Counter

        Counter翻译过来为计数器,它也继承自dict,它接受一个可迭代对象并计数,计数方法如下

    def update(self, iterable=None, /, **kwds):
        if iterable is not None:
            if isinstance(iterable, _collections_abc.Mapping):    # Mapping的实例,有items方法
                if self:    # 若已经实例化了Counter再update
                    self_get = self.get    # 直接使用实例的get方法
                    for elem, count in iterable.items():
                        # 把mapping对象的键值对依次添加到self中
                        self[elem] = count + self_get(elem, 0)
                else:
                    # fast path when counter is empty
                    super().update(iterable)
            else:
                _count_elements(self, iterable)
        if kwds:
            self.update(kwds)

    def _count_elements(mapping, iterable):
        # 这是把一个可迭代对象的值依次计数的方法
        mapping_get = mapping.get
        for elem in iterable:
            mapping[elem] = mapping_get(elem, 0) + 1

        源码中的核心是一行代码,dc[v] = dc.get(v, 0) + 1,这实际上利用了get方法在键不存在时会返回传入的第二个参数的特点,注意,当第二个参数需要实时计算时,需要慎重考虑使用这种方法,因为无论键是否存在,第二个参数都会被计算,可能会造成性能问题;

dc = {'a': 1, 'b': 2, 'c': 3}

iterable = 'xyuawxgwuaixawfawf'
for v in iterable:
    dc[v] = dc.get(v, 0) + 1
print(dc)  # {'a': 5, 'b': 2, 'c': 3, 'x': 3, 'y': 1, 'u': 2, 'w': 4, 'g': 1, 'i': 1, 'f': 2}

        它与我们通过dict实现的简单计数器还有一个重要区别,它可以这么使用:

from collections import Counter, defaultdict

cnt = Counter()
cnt['a'] += 1
print(cnt)    # Counter({'a': 1})

dd = defaultdict(int)
dd['a'] += 1
print(dd)    # defaultdict(<class 'int'>, {'a': 1})

        有点像defaultdict使用int作为工厂,那么Counter是不是重写了__getitem__呢,我们可以去阅读源码,发现并没有,其实是利用了另一个魔术方法__missing__,只要key不存在就返回0,为什么可以执行+=呢,这就需要我们查看这个赋值语句的字节码了,在此就不再展开了,感兴趣的可以查询python的dis模块来获取相关知识

    def __missing__(self, key):
        'The count of elements not in the Counter is zero.'
        # Needed so that self[missing_item] does not raise KeyError
        return 0

        它还重写了__iadd__等等方法(+=),可以自行查看,还有一些dict没有的方法,比如total、most_common(用到了数据结构堆)等,源码注释都非常详细,不再赘述:

    def __iadd__(self, other):
        # 可以让两个Counter直接相加,等到计数的并集
        for elem, count in other.items():
            self[elem] += count
        return self._keep_positive()

  • 26
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值