OrderedDict随笔

背景

今日在工作中阅读业务代码时,发现用到了OrderedDict类,遂思考如何实现OrderedDict,此文记录思考与实现过程。

OrderedDict意思上为有序字典,何为有序字典?python中普通的dict在插入数据时,最终数据存储的顺序非插入顺序。例如:

a = dict()
a['a'] = 1
a['b'] = 2
a['c'] = 3

当我们在使用for循环遍历时,会发现打印出来的结果和我们插入顺序不同。

image-20220106101732638.png

当然,这和字典底层使用hash表进行存储关系密切,此文暂不深入展开字典的底层实现。

有序字典通常在需要记录数据插入顺序,且能够快速定位到元素的场景使用。例如:记账。

记账这个功能中,我们需要按照时间顺序存储每一笔消费,且能够通过日期快速定位到当天的消费记录。因此,选择有序字典进行数据存储是比较理想的实现方法。

实现过程

实现OrderedDict过程中,首先想到的即是数组,因为数组的append操作可以保证插入顺序性,而需要使用高效查找,则需要结合字典共同实现,在字典中存储插入项在数组中的下标,这样就可以通过key快速定位到数组中的元素。

第一版实现如下,完成通过[]set和get值的功能:

class OrderedDict():
    def __init__(self):
        self.elements = []
        self.index = {}

    def __setitem__(self, key, value):
        if key not in self.index:
            self.elements.append((key, value))
            self.index[key] = len(self.elements) - 1
        else:
            pos = self.index[key]
            self.elements[pos] = (key, value)

    def __getitem__(self, key):
        if key not in self.index:
            raise KeyError(key)

        pos = self.index[key]
        return self.elements[pos][1]

第二版希望实现keys()items()方法,并且item()方法返回生成器,代码如下:

class OrderedDict():
    def __init__(self):
        self.elements = []
        self.index = {}

    def __setitem__(self, key, value):
        if key not in self.index:
            self.elements.append((key, value))
            self.index[key] = len(self.elements) - 1
        else:
            pos = self.index[key]
            self.elements[pos] = (key, value)

    def __getitem__(self, key):
        if key not in self.index:
            raise KeyError(key)

        pos = self.index[key]
        return self.elements[pos][1]

    def items(self):
        for element in self.elements:
            yield element[0], element[1]

    def keys(self):
        all_key = []
        for element in self.elements:
            all_key.append(element[0])

        return all_key

第三版希望实现通过for循环遍历该有序字典,遂初次写入如下代码:

class OrderedDict():
    def __init__(self):
        self.elements = []
        self.index = {}

    def __setitem__(self, key, value):
        if key not in self.index:
            self.elements.append((key, value))
            self.index[key] = len(self.elements) - 1
        else:
            pos = self.index[key]
            self.elements[pos] = (key, value)

    def __getitem__(self, key):
        if key not in self.index:
            raise KeyError(key)

        pos = self.index[key]
        return self.elements[pos][1]

    def __iter__(self):
        return self

    def next(self):
        for element in self.elements:
            yield element[0]

    def items(self):
        for element in self.elements:
            yield element[0], element[1]

    def keys(self):
        all_key = []
        for element in self.elements:
            all_key.append(element[0])

        return all_key

执行后发现出现无限循环:


image-20220106200518102.png

再次分析for循环的执行原理:通过iter()拿到可迭代对象,再通过next()方法不断获取元素,直到捕获StopIteration异常。

于是发现自己写的next()方法每次执行都会返回生成器,而非元素,故想:需要在next()方法外部保存一个计数器,指向当前遍历的元素位置,每次执行next()方法,都获取到该计数器指向的元素。

思考一番,将可迭代对象的实现移到另一个OrderedDictIterator类中,在iter()方法返回该类对象。

完整代码:

import unittest


class OrderedDictIterator():
    def __init__(self, iter_cols):
        self.iter_cols = iter_cols
        self.iter_start = 0

    def next(self):
        if self.iter_start < len(self.iter_cols):
            value = self.iter_cols[self.iter_start]

            self.iter_start += 1
            return value

        else:
            raise StopIteration()

    __next__ = next


class OrderedDict():
    def __init__(self):
        self.elements = []
        self.index = {}

    def __setitem__(self, key, value):
        if key not in self.index:
            self.elements.append((key, value))
            self.index[key] = len(self.elements) - 1
        else:
            pos = self.index[key]
            self.elements[pos] = (key, value)

    def __getitem__(self, key):
        if key not in self.index:
            raise KeyError(key)

        pos = self.index[key]
        return self.elements[pos][1]

    def __iter__(self):
        return OrderedDictIterator(self.elements)

    def items(self):
        for element in self.elements:
            yield element[0], element[1]

    def keys(self):
        all_key = []
        for element in self.elements:
            all_key.append(element[0])

        return all_key

        

class TestOrderedDict(unittest.TestCase):

    def test_add_element(self):
        d = OrderedDict()
        d['a'] = 1
        d['b'] = 2

    def test_get_element(self):
        d = OrderedDict()
        d['a'] = 1
        self.assertEqual(d['a'], 1)

    def test_set_element(self):
        d = OrderedDict()
        d['a'] = 1
        d['a'] = 2
        self.assertEqual(d['a'], 2)

    def test_for(self):
        d = OrderedDict()
        d['a'] = 1
        d['b'] = 2
        for key, value in d.items():
            print('key: {}, value: {}'.format(key, value))

    def test_keys(self):
        d = OrderedDict()
        d['a'] = 1
        self.assertEqual(d.keys(), ['a'])

    def test_for2(self):
        d = OrderedDict()
        d['a'] = 1
        for i in d:
            print(i)


if __name__ == '__main__':
    unittest.main()

后续

上述方法采用外部类的next()方法实现迭代,从stackoverflow评论中看到他人评论,可直接在__iter__()方法中yield出元素,实现迭代。

此处给出简单实现:

def __iter__(self):
    for element in self.elements:
        yield element[0]

总结

1、实现某个对象可以被for循环输出时,在python2中使用next(),而在python3中使用__next__()方法,如果代码需要兼容2和3,则可以在类中实现next()方法,并使用__next__ = next的方式完成2和3的兼容

2、当for循环的目的是为了遍历对象中的存储的集合数据类型时,最好再实现一个类相关的迭代器,而不是在当前类中实现next()方法。

3、next()方法在每次for循环时都会被调用,需要有计数器来控制迭代次数,否则会出现无限循环。

4、next()方法不一定需要实现,在__iter__()方法中通过yield返回元素也可以实现for循环。

参考

1、python iterator

2、https://stackoverflow.com/questions/5982817/problems-using-next-method-in-python

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,335评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,895评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,766评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,918评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,042评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,169评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,219评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,976评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,393评论 1 304
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,711评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,876评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,562评论 4 336
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,193评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,903评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,142评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,699评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,764评论 2 351

推荐阅读更多精彩内容

  • CD-Python-JY-1809班项目阶段教学内容 开篇 - 就业形势分析 就业方向Python后端开发工程师(...
    ychaochaochao阅读 940评论 0 1
  • Python后端面试 Python后端技术栈 Web请求的流程 浏览器 负载均衡 Web框架 业务逻辑 数据库缓存...
    dreamkong阅读 5,623评论 1 10
  • 字符串格式化调用方法 —— format 通过创建字符串模板,利用format函数,替代相应的值。 可以通过绝对位...
    plutoese阅读 1,511评论 0 47
  • python学习笔记 声明:学习笔记主要是根据廖雪峰官方网站python学习学习的,另外根据自己平时的积累进行修正...
    renyangfar阅读 3,031评论 0 10
  • 过滤列表中的数据 实际案例: 过滤掉列表里面的负数 案例分析: filter(function or None, ...
    finlu阅读 1,500评论 0 0