• 字典高级用法


    字典高级用法

    1.概述

    这篇文章总结在开发中使用字典的一些技巧。

    2.字典常用操作

    字典存储的内容不是单一维度的线性序列,而是多维度的key: value键值对。

    2.1.创建字典

    字典也有两种定义方式,字面量表达式和dict() 内置函数。

    # 字面量表达式创建字典
    movie = {'name': 'Burning', 'type': 'movie', 'year': 2018}
    
    # 内置函数创建字典
    d = dict(name='Burning', type='movie')
    
    • 1
    • 2
    • 3
    • 4
    • 5

    2.2.遍历字典

    # 遍历字典获取key和value
    movie = {'name': 'Burning', 'type': 'movie', 'year': 2018}
    for key, value in movie.items():
        print(f'遍历输出字典的key和value:{key}: {value}')
    
    • 1
    • 2
    • 3
    • 4

    3.字典高级操作

    3.1.访问字典不存在的键

    当用不存在的键访问字典内容时,程序会抛出KeyError异常,我们称为程序的边界情况。常用处理方式有两种。

    • 读取内容前先做一次判断,只有判断通过才做其他操作。
    • 直接操作,捕获Key Error 异常
    # 获取字典中let键的值,如果该键不存在则返回一个默认值
    # 第一种写法判断key是否存在
    if 'let' in movie:
        let = movie['let']
    else:
        let = 0
    
    # 第二种写法捕获异常
    try:
        let = movie['let']
    except KeyError:
        let = 0
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    除了上面的两种方法,字典还提供了一个get方法获取key的值,它接收两个参数,其中default参数,当访问的key不存在时,方法会返回default默认值。

    # 当key不存在时返回设置的默认值
    movie.get('let', 0)
    
    • 1
    • 2

    3.2.使用setdefault取值并修改

    有时,我们需要修改字典中某个可能不存在的键,比如下面的代码示例,我需要往字典movie的hobby键里追加新值,但movie[‘hobby’] 的键可能不存在。因此需要写一段异常捕捉逻辑,如下面示例。

    movie = {'name': 'Burning', 'type': 'movie', 'year': 2018}
    try:
        movie['hobby'].append('读书')
    except KeyError:
        movie['hobby'] = ['读书']
    print(f'字典添加新值:{movie}')
    # 输出结果
    字典添加新值:{'name': 'Burning', 'type': 'movie', 'year': 2018, 'hobby': ['读书']}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    针对上面的情况,还有一个更适合的工具:setdefault(key, default=None) 方法。使用它可以直接删掉上面异常捕获,代码会变得更简单。
    setdefault(key, default=None) 方法会产生两种结果,当key不存在时,该方法会把default值写入字典的key位置,并返回该值;当key存在时,该方法就会直接返回字典中的值。

    movie = {'name': 'Burning', 'type': 'movie', 'year': 2018}
    movie.setdefault('hobby',[]).append('读书')
    print(f'setdefault方法添加新值:{movie}')
    # 输出结果
    setdefault方法添加新值:{'name': 'Burning', 'type': 'movie', 'year': 2018, 'hobby': ['读书']}
    
    • 1
    • 2
    • 3
    • 4
    • 5

    3.3.使用pop方法删除不存在的键

    当删除字典中某个键,一般会使用del d[key] ;但如果要删除的键不存在就会抛出KeyError异常,因此要安全的删除某个键,需要加上一段异常捕获。

    movie = {'name': 'Burning', 'type': 'movie', 'year': 2018}
    # del删除不存在的键会报KeyError异常
    del movie['hobby']
    # 异常信息
    KeyError: 'hobby'
    
    # 捕获异常
    try:
        del movie['hobby']
    except KeyError:
        print(KeyError)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    假如只是想去掉某个键,不关系它存在与否,删除是否成功,使用dict.pop(key, default) 方法代码更简单,省去了捕获异常的代码,同时当键不存在时也不会抛出异常。

    movie.pop('year',None)
    print(f'使用pop删除键:{movie}')
    # 输出结果
    使用pop删除键:{'name': 'Burning', 'type': 'movie'}
    
    movie.pop('hobby', None)
    print(f'使用pop删除不存在的键:{movie}')
    # 输出结果
    使用pop删除不存在的键:{'name': 'Burning', 'type': 'movie'}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    3.4.字典推导式

    字典和列表一样都有自己的推导式,可以用它来过滤和处理字典成员。

    movie = {'name': 'Burning', 'type': 'movie', 'year': 2018}
    d = {key:value for key, value in movie.items() if key == 'year'}
    print(f'列表推导式,创建一个新的字典:{d}')
    
    • 1
    • 2
    • 3

    3.5.认识字典有序性与无序性

    在Python3.6版本以前,python的字典是无序的。例如当你按照某种顺序把内容放进字典后,获取字典内容时顺序就变量,这是因为字典的key是按照hash值排序。但Python在进化,Python3.6版本为字典引入了一个改进,优化了底层实现,同时实现了存放数据和读取数据顺序一致。
    在python的collections模块里还有一个有序字典对象OrderedDict,他可以保证在在3.6之前的版本里字典是有序的。

    既然普通的字典已经实现了有序性,那么OrderedDict是否还有必要保留吗。其实他们还是有一个细微的差别的,比如在对比两个内容相同而顺序不同的字典时,普通对象会返回True,而OrderedDict会返回False。

    d1 = {'a': 'a', 'b': 'b', 'c': 'c'}
    d2 = {'c': 'c', 'a': 'a', 'b': 'b'}
    
    print(f'对比两个普通字典对象是否一致: {d1 == d2}')
    # 输出结果
    对比两个普通字典对象是否一致: True
    
    from collections import OrderedDict
    
    d1 = OrderedDict(a='a', b='b')
    d2 = OrderedDict(b='b', a='a')
    print(f'对比两个有序字典对象是否一致: {d1 == d2}')
    # 输出结果
    对比两个有序字典对象是否一致: False
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    3.6.分析日志案例优化

    案例特点

    • 使用defaultdict特殊字典类型解决向字典增加新值key不存在问题。
    • 自定义字典类型新增定制化功能,提升字典能力。

    1.日志分析需求描述

    2.日志分析第一个版本

    # 日志分析案例
    from enum import Enum
    
    
    class PagePerflevel(str, Enum):
        LT_100 = 'Less than 100ms'
        LT_300 = 'Between 100 and 300ms'
        LT_1000 = 'Between 300 and 1s'
        Gt_1000 = 'Greater than 1s'
    
    
    def analyze_v1():
        with open("analyze_log.txt", "r") as fp:
            path_group = {}
            # 格式化日志内容
            for line in fp:
                path, cost_time = line.strip().split()
                # 将响应时间转化为对应的等级
                cost_time = int(cost_time)
                if 100 > cost_time:
                    level = PagePerflevel.LT_100
                elif 100 < cost_time < 300:
                    level = PagePerflevel.LT_300
                elif 300 < cost_time < 1000:
                    level = PagePerflevel.LT_1000
                elif 1000 < cost_time:
                    level = PagePerflevel.Gt_1000
    
                # 按照路径分类,统计不同耗时等级的次数
                if path not in path_group:
                    path_group[path] = {}
    
                try:
                    path_group[path][level] += 1
                except KeyError:
                    path_group[path][level] = 1
    
            # 输出结果
            for path, result in path_group.items():
                print(f'path: {path}')
                total = sum(result.values())
                print(f'    Total request: {total}')
                print(f'    Performance:')
                sorted_item = sorted(result.items(), key=lambda pair: list(PagePerflevel).index(pair[0]))
                for level_name, count in sorted_item:
                    print(f'    - {level_name}: {count}')
    
    analyze_v1()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48

    日志文件内容

    /api/home/ 120
    /api/home/ 100
    /api/home/ 200
    /api/home/ 400
    /api/user/ 100
    /api/user/ 200
    /api/user/ 300
    /api/user/ 300
    /api/shop/ 100
    /api/shop/ 150
    /api/shop/ 200
    /api/shop/ 2000
    /api/shop/ 2000
    /api/shop/ 2000
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    3.日志分析脚本缺点分析

    • 1.日志分析所有逻辑都放在一个函数中,包含读取文件、耗时转换为级别、请求数累加、输出结果,整个函数读起来不够简介,扩展性不好。
    • 2.代码里分布着太多零碎的字典操作,比如 if path not in path_groups 等等看上去不利索。

    4.日志分析脚本优化思路

    • 1.在代码中有两处字典操作非常类似,为了正常处理他们分别是采用if 和 try 两种方式判断字典中key是否存在,在存入数据。其实还可以使用字典的get()和setdefault()方法来简化代码。但是在这个场景下,内置模块的collections里的defaultdict类型才是最好的选择。
    • 2.使用MutableMapping创建自定义字典,将耗时转为level等级保存到字典中。

    5.defaultdict类型字典

    defaultdict(default_factory, …) 是一种特殊的字典类型,他在被初始化时,接收一个可调用对象default_factory作为参数。之后每次操作字典时,如果访问的key不存在,defaultdict对象会自动调用default_factory()并将结果作为值保存在对应的key里。
    为了更好理解defaultdict类型字典,下面是一个使用示例。

    # defaultdict类型字典
    from collections import defaultdict
    int_dict = defaultdict(int)
    print(f'初始化defaultdict类型字典,参数类型是int:{int_dict}')
    int_dict['foo'] += 1
    print(f'字典赋值,当key foo不存在时,自动调用default_factory也就是int()初始化值为0,然后赋值。foo的值为:{int_dict}')
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    6.MutableMapping创建自定义字典

    自定义字典和普通字典很像,但它可以给字典的默认行为添加一些自定义功能。比如我们让字典的操作 “响应耗时” 键时自动转为性能等级。

    # 创建自定义字典
    from collections.abc import MutableMapping
    class PerfLevelDict(MutableMapping):
        def __init__(self):
            self.data = defaultdict(int)
    
        def __getitem__(self, key):
            return self.data[self.compute_level(key)]
    
        def __setitem__(self, key, value):
            self.data[self.compute_level(key)] = value
    
        def __delitem__(self, key):
            del self.data[key]
    
        def __iter__(self):
            return iter(self.data)
    
        def __len__(self):
            return len(self.data)
    
        @staticmethod
        def compute_level(time_cost_str):
        	'''
        	根据响应耗时转换为性能等级,如果传来的值已经是性能等级,不做
        	转换直接返回。
        	'''
            if time_cost_str in list(PagePerflevel):
                return time_cost_str
            time_cost = int(time_cost_str)
            if time_cost < 100:
                return PagePerflevel.LT_100
            elif time_cost < 300:
                return PagePerflevel.LT_300
            elif time_cost < 1000:
                return PagePerflevel.LT_1000
            return PagePerflevel.Gt_1000
    
    d = PerfLevelDict()
    d[50] += 1
    d[500] += 2
    print(dict(d))
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42

    在上面代码中,编写了一个继承了MutableMapping的字典类PerfLevelDict。但是光继承还不够,要让这个类变得像字典一样,需要重写包括 getitemsetitem、在内的6个魔法方法。下面介绍下重要的几个步骤

    • 在__init__初始化方法里,使用defaultdict(int)对象来简化字典的空值初始化操作。
    • __getitem__方法定义了 d[key] 取值的操作行为
    • __setitem__方法定义了d[key] = value 赋值操作时的行为
    • PerfLevelDict的__getitem__和__setitem__方法和普通字典的最大不同,在于操作前调用了compute_level(),将字典转成了性能等级。

    7.日志分析脚本重构

    from collections import defaultdict
    from collections.abc import MutableMapping
    from enum import Enum
    
    
    class PagePerfLevel(Enum):
        LT_100 = "Less than 100 ms"
        LT_300 = "Between 100 and 300 ms"
        LT_1000 = "Betten 300ms and 1 s"
        GT_1000 = "Greater than 1 s"
    
    class PerfLevelDict(MutableMapping):
        def __init__(self):
            self.data = defaultdict(int)
            print("---",self.data)
    
        def __getitem__(self, key):
            get_data = self.data[self.compute_level(key)]
            print("get_data",get_data)
            return get_data
    
        def __setitem__(self, key, value):
            print("key",key)
            print("value",value)
            self.data[self.compute_level(key)] = value
            print("data",self.data)
            print("data key",self.data.keys())
    
        def __delitem__(self, key):
            del self.data[key]
    
        def __iter__(self):
            return iter(self.data)
    
        def __len__(self):
            return len(self.data)
    
        def items(self):
            return sorted(
                self.data.items(),
                key=lambda pair:list(PagePerfLevel).index(pair[0])
            )
        def total_requests(self):
            return sum(self.values())
    
        @staticmethod
        def compute_level(time_cost_str):
            if time_cost_str in list(PagePerfLevel):
                return time_cost_str
    
            time_cost = int(time_cost_str)
            if time_cost<100:
                print(PagePerfLevel.LT_100)
                return PagePerfLevel.LT_100
            elif time_cost<300:
                return PagePerfLevel.LT_300
            elif time_cost<1000:
                return PagePerfLevel.LT_1000
            return PagePerfLevel.GT_1000
    
    def analy_2():
        # 初始化一个空的defaultdict字典,default_factory参数类型为PerfLevelDict
        path_groups = defaultdict(PerfLevelDict)
        with open("analyze_log.txt", "r") as fp:
            for line in fp:
                path,time_cost = line.strip().split()
                # 使用defaultdict字典添加key和value
                path_groups[path][time_cost] +=1
    
        for path, result in path_groups.items():
            print(f'==Path: {path}')
            # result对象类型是 PerfLevelDict 因此可以调用total_requests方法
            print(f' Total request: {result.total_requests()}')
            print(f'Performance')
            for level_name, count in result.items():
                print(f' {level_name}:{count}')
    
    
    if __name__ == '__main__':
        analy_2()
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81

    3.7.合并字典

    合并字典使用**运算符来解包,在字典中可以动态解包字典任何内容,并与当前字典合并

    d1 = {'name': 'apple'}
    d2 = {'foo': 'banana'}
    d3 = {**d1, **d2}
    print(d3)
    # 输出结果
    {'name': 'apple', 'foo': 'banana'}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    除了使用**解包字典,可以使用单星号解包任何可迭代对象

    l1 = [1, 2]
    l2 = [3, 4]
    l3 = [*l1, *l2]
    
    • 1
    • 2
    • 3

    3.8.使用有序字典去重

    集合里的成员是不会重复的,因此经常使用它来去重。但是,使用集合去重有一个很大的缺点,得到的结果会丢失原有的排序。
    如果既需要去重,有要保持排序,可以使用有序字典来去重。
    OrderedDict同时满足两个条件

    • 他的键是有序的
    • 他的键是不会重复的

    因此只要根据列表构建一个字典,它的键就是有序去重的结果。

    # 有序字典去重
    nums = [10, 2, 3, 10, 4, 6]
    from collections import OrderedDict
    # 调用fromkeys方法会创建一个有序字典对象,值默认为None
    l = list(OrderedDict.fromkeys(nums).keys())
    print(l)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    4.不可变字典类型

    标准库里所有的映射类型都是可变的,但有时候你会有这样的需求,比如不能让用户错误地修改某个映射。
    假如给客户端暴露一个字典对象,里面的数据都是提前初始化好的,并且不希望调用方修改字典中的数据,只能访问字典中的数据。例如给客户端返回一个错误代码集合,包含了错误号代表了不同的含义。

    从Python 3.3开始,types模块中引入了一个封装类名叫MappingProxyType。如果给这个类一个映射,它会返回一个只读的映射视图。虽然是个只读视图,但是它是动态的。这意味着如果对原映射做出了改动,我们通过这个视图可以观察到,但是无法通过这个视图对原映射做出修改。

    from types import MappingProxyType
    
    d = {'a': 1, 'b': 2}
    d_proxy = MappingProxyType(d)
    
    print(f'输出不可变类型字典内容:{d_proxy}')
    # 输出
    输出不可变类型字典内容:{'a': 1, 'b': 2}
    
    print(f'读取不可变类型字典内容:{d_proxy["a"]}')
    #输出
    读取不可变类型字典内容:1
    
    d['c'] = 3
    print(f'原字典内容可修改:{d_proxy}')
    # 输出
    原字典内容可修改:{'a': 1, 'b': 2, 'c': 3}
    
    d_a3 = d_proxy["a"] = 3
    print('不可变类型字典不可修改内容:', d_a3)
    #输出
    TypeError: 'mappingproxy' object does not support item assignment
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
  • 相关阅读:
    switch&循环语句
    SVN常用操作
    leetcode 简单
    队列(循环数组队列,用队列实现栈,用栈实现队列)
    188.买卖股票的最佳时机Ⅳ【Java】
    Java.lang.Character类中isDigit()方法具有什么功能呢?
    排序——归并排序
    双非温州大学新增电子信息专硕,考408!
    放大器的输入、输出电压范围的理解
    vue中babel-plugin-component按需引入和element-ui 的主题定制,支持发布,上线
  • 原文地址:https://blog.csdn.net/m0_38039437/article/details/126491381