美文网首页
OrderedDict随笔

OrderedDict随笔

作者: Debris丶 | 来源:发表于2022-01-06 21:52 被阅读0次

    背景

    今日在工作中阅读业务代码时,发现用到了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

    相关文章

      网友评论

          本文标题:OrderedDict随笔

          本文链接:https://www.haomeiwen.com/subject/bkldcrtx.html