(一)enum:枚举类型
import enum ''' enum模块定义了一个提供迭代和比较功能的枚举类型。可以用这个为值创建明确定义的符号,而不是使用字面量整数或字符串 '''
1.创建枚举
import enum ''' 可以使用定义一个类,继承自Enum,来实现枚举 ''' class Color(enum.Enum): red = 1 green = 2 yellow = 3 cyan = 4 purple = 5 # 此时我们可以直接通过类名来调用里面的元素,里面的元素有两个属性,一个是name,一个是value print(Color.red) # Color.red print(Color.red.name) # red print(Color.red.value) # 1
2.迭代
import enum ''' 可以迭代处理枚举的各个成员 ''' class Color(enum.Enum): red = 1 green = 2 yellow = 3 cyan = 4 purple = 5 for c in Color: print(c, c.name, c.value) ''' Color.red red 1 Color.green green 2 Color.yellow yellow 3 Color.cyan cyan 4 Color.purple purple 5 ''' # 可以看到打印是由顺序的,就是我们添加的顺序
3.比较Enum
import enum ''' 枚举类型只能比较是否相等,不能进行大小判断 ''' class Color(enum.Enum): red = 1 green = 2 yellow = 3 cyan = 4 purple = 5 print(Color.red == Color.cyan) # False print(Color.red == Color.red) # True print(Color.red is Color.red) # True # 注意:如果进行大小比较,会抛出一个TypeError try: Color.red > Color.cyan except TypeError as err: print(err) # '>' not supported between instances of 'Color' and 'Color' # 但如果我非要比较呢?那就不能继承Enum了,需要继承IntEnum class Color(enum.IntEnum): red = 1 green = 2 yellow = 3 cyan = 4 purple = 5 # 会自动将值进行比较 print(Color.cyan < Color.purple) # True # 会自动将值进行相加 print(Color.red + Color.green) # 3
4.唯一枚举值
import enum ''' 有相同值得Enum成员会被处理为同一个对象的别名引用,别名可以避免Enum的迭代器中出现相同的值 ''' class Color(enum.Enum): red = 1 green = 2 yellow = 3 cyan = 4 purple = 5 black = 3 for c in Color: print(c.name) ''' red green yellow cyan purple ''' # 可以看到black并没有被打印出来,因为它和yellow的值一样,所以将其当做了yellow的别名 # 至于为什么是black被当做了yellow的别名,而不是yellow被当做black的别名,原因很简单,因为black在下面 print(Color.yellow is Color.black) # True # 并且这里要提一点,值是可以重复的,只是会被当做别名,但是name是不能重复的,一旦重复,必定报错 # 那如果我想值也不能重复呢?很简单, 只需要加上一个装饰器即可 try: @enum.unique class Color(enum.Enum): red = 1 green = 2 yellow = 3 cyan = 4 purple = 5 black = 3 except ValueError as err: print(err) # duplicate values found in <enum 'Color'>: black -> yellow
(二)collections:容器数据类型
import collections ''' collections模块包含除内置类型list、dict和tuple等意外的其他容器数据类型 '''
1.ChainMap:搜索多个字典
from collections import ChainMap ''' ChainMap类一个字典序列,并按照其出现的顺序搜索以查找与键关联的值。 ChainMap提供了一个很好的上下文容器,因为可以把它看做成一个栈,栈增长时发生变更,栈收缩时这些变更将被丢弃 ''' d1 = {"a": 12, "b": 22, "c": 33} d2 = {"b": 1, "c": 2, "d": 3} d = ChainMap(d1, d2) for k, v in d.items(): print(k, v) ''' b 22 c 33 d 3 a 12 ''' # 可以看到打印的结果是无序的,而且如果多个字典中有相同的key,那么只保留第一次出现的key # 并且ChainMap有一个maps属性,存储了要搜索的映射列表。这个列表是可变的。所以可以直接增加新映射,或者改变元素的顺序以控制查找和更新行为。 print(d.maps) # [{'a': 12, 'b': 22, 'c': 33}, {'b': 1, 'c': 2, 'd': 3}] # 这是我们存储的信息,如果在d.maps里面修改了,那么会怎么样呢? print(d1) # {'a': 12, 'b': 22, 'c': 33} d.maps[0]["a"] = "yoyoyo" # 可以看到d.maps里面存储的只是一个引用,因此改变之后会影响原来的结果 print(d1) # {'a': 'yoyoyo', 'b': 22, 'c': 33} # 那我如果改变了原来的值,会不会影响d.maps呢?显然是可以的,毕竟同一个内存地址嘛 d2["d"] = "我屮艸芔茻" print(d.maps) # [{'a': 'yoyoyo', 'b': 22, 'c': 33}, {'b': 1, 'c': 2, 'd': '我屮艸芔茻'}]
2.Counter:统计可散列的对象
from collections import Counter ''' Counter是一个容器,可以计算出序列中每一个元素出现的次数 ''' # 初始化 print(Counter("aabbbc")) # Counter({'b': 3, 'a': 2, 'c': 1}) print(Counter(['a', 'a', 'b', 'b', 'b', 'c'])) # Counter({'b': 3, 'a': 2, 'c': 1}) print(Counter(a=2, b=3, c=1)) # Counter({'b': 3, 'a': 2, 'c': 1}) c = Counter("aaabbc") # 表示a出现了三次,b出现了两次,c出现了一次 print(c) # Counter({'a': 3, 'b': 2, 'c': 1}) # 可以进行填充 c.update("bcd") # 可以看到b和c的值都增加了1,并且出现了d print(c) # Counter({'a': 3, 'b': 3, 'c': 2, 'd': 1}) # 访问计数,Counter对象可以像字典一样访问 print(c["a"]) # 3 # 如果访问一个不存在的key,不会引发KeyError,而是会返回0,表示对象中没有这个key print(c["mmp"]) # 0 # 还可以使用elements进行迭代,会得到Counter对象中的所有元素 print(list(c.elements())) # ['a', 'a', 'a', 'b', 'b', 'b', 'c', 'c', 'd'] # 还可以计算出现最多的元素 # 统计string中前三个出现次数最多的元素 string = "sasaxzsdsadfscxzcasdscxzdfscxsasadszczxczxcsds" c = Counter(string) print(c) # Counter({'s': 13, 'c': 7, 'a': 6, 'x': 6, 'z': 6, 'd': 6, 'f': 2}) print(c.most_common(3)) # [('s', 13), ('c', 7), ('a', 6)] # Counter还可以进行算数操作 c1 = Counter("aabbccc") c2 = Counter("bbbccdd") print(c1) # Counter({'a': 2, 'b': 2, 'c': 3}) print(c2) # Counter({'b': 3, 'c': 2, 'd': 2}) # 如果c1的元素出现在了c2中,就把该元素减去,记住:减的是次数 print(c1 - c2) # Counter({'a': 2, 'c': 1}) ''' a在c1中出现了2次,c2中没有出现,所有是a: 2。b在c1中出现两次,在c2中出现3次,所以一减就没有了。 而c在c1中出现了三次,在c2中出现两次,所以相减还剩下一次。至于c1没有的元素就不用管了 ''' # 相加就很好理解了 print(c1 + c2) # Counter({'b': 5, 'c': 5, 'a': 2, 'd': 2}) # 相交的话,查找公共的元素,并且取次数出现较小的那个 print(c1 & c2) # Counter({'b': 2, 'c': 2}) # 并集的话,取较大的,记住不是相加,所以b和c出现的次数不会增加,只是取较大的那个、 print(c1 | c2) # Counter({'b': 3, 'c': 3, 'a': 2, 'd': 2})
3.defaultdict:缺少的键返回一个默认值
from collections import defaultdict ''' 标准字典中有setdefault和get,可以用来获取key对应的value。 如果key存在,两者会获取key对应的value 但如果key不存在,setdefault就会先将key和指定的默认值设置进去,然后返回一个默认值。 而get则只会返回默认值,。不会设置值 example: d = {"a": 1} print(d.get("a", 0)) # 1 print(d.setdefault("a", 0)) # 1 print(d) # {"a": 1} print(d.get("b", 0)) # 0 print(d) # {"a": 1} print(d.setdefault("b", 0)) # 0 print(d) # {"a": 1, "b": 0} 所以这里相当于执行了两步操作。先将("b", 0)设置到字典里,然后再获取 defaultdict在初始化的时候就会让调用者提前指定默认值 ''' s = "aabbccdddddee" d1 = {} for c in s: d1.setdefault(c, 0) d1[c] += 1 print(d1) # {'a': 2, 'b': 2, 'c': 2, 'd': 5, 'e': 2} # 如果使用defaultdict的话呢? d2 = defaultdict(int) print(d2["a"]) # 0 d2 = defaultdict(str) print("%r" % d2["a"]) # '' d2 = defaultdict(tuple) print(d2["a"]) # () d2 = defaultdict(list) print(d2["a"]) # [] # 如果获取不到key,那么会自动输出传入类型所对应的零值.能获取到key,输入key对应的value值 s = "aabbccdddddee" d2 = defaultdict(int) for c in s: ''' 一开始没有值,设置为0,然后每来一个值就加上1 ''' d2[c] += 1 print(d2) # defaultdict(<class 'int'>, {'a': 2, 'b': 2, 'c': 2, 'd': 5, 'e': 2}) # 此外还可以自定义,只需要添加一个不需要参数的函数即可,指定一个返回值 d3 = defaultdict(lambda: "default") print(d3["aa"]) # default # 此外还可以添加参数 d4 = defaultdict(lambda: "default", aa="bar") print(d4["aa"]) # bar # 这种字典是如何实现的呢?主要是内部实现了一个__missing__魔法方法 class MyDict(dict): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) def __getitem__(self, item): value = super().__getitem__(item) # 会执行父类的__getitem__方法,如果获取不到 # 会检测我们是否定义__missing__方法,如果有,执行。没有,报错 # 所以这里的value就是__missing__方法的返回值 return value def __missing__(self, key): self[key] = "为什么渣男这么吃香,像我们这样暖人心的老实人却不受待见" return self[key] d = MyDict([("a", 3), ("b", 4)]) print(d) # {'a': 3, 'b': 4} print(d["mmm"]) # 为什么渣男这么吃香,像我们这样暖人心的老实人却不受待见
4.deque:双端队列
from collections import deque ''' 双端队列支持从任意一端增加和删除元素。更为常用的两种数据结构(即栈和队列)就是双端队列的退化形式,它们的输入和输出被限制在某一端 ''' d = deque("abcdefg") print(d) # deque(['a', 'b', 'c', 'd', 'e', 'f', 'g']) print(len(d)) # 7 print(d[0]) # a print(d[-1]) # g # 由于deque是一种序列容器,因此同样支持list的操作。如:通过索引获取元素,查看长度,删除元素,反转元素等等 # list支持的deque基本上都支持 d.reverse() print(d) # deque(['g', 'f', 'e', 'd', 'c', 'b', 'a']) d.remove("c") print(d) # deque(['g', 'f', 'e', 'd', 'b', 'a']) # 填充元素 # 首先可以像list一样添加元素,但是deque可以从两端添加 d.append("yoyoyo") # 默认和list一样,在尾部添加 d.appendleft("哟哟哟") # 也可以添加在头部 print(d) # deque(['哟哟哟', 'g', 'f', 'e', 'd', 'b', 'a', 'yoyoyo']) # 还可以使用insert, 如果范围越界,自动添加在两端 d.insert(100, "x") print(d) # deque(['哟哟哟', 'g', 'f', 'e', 'd', 'b', 'a', 'yoyoyo', 'x']) # extend,extendleft d1 = [1, 2, 3] d2 = deque([4, 5, 6]) d.extend(d1) print(d) # deque(['哟哟哟', 'g', 'f', 'e', 'd', 'b', 'a', 'yoyoyo', 'x', 1, 2, 3]) d.extendleft(d2) print(d) # deque([6, 5, 4, '哟哟哟', 'g', 'f', 'e', 'd', 'b', 'a', 'yoyoyo', 'x', 1, 2, 3]) # 可以看到extend也支持从左端添加,而且不仅仅可以添加deque,任意序列类型都是可以的。 d.extendleft("我屮艸芔茻") print(d) # deque(['茻', '芔', '艸', '屮', '我', 6, 5, 4, '哟哟哟', 'g', 'f', 'e', 'd', 'b', 'a', 'yoyoyo', 'x', 1, 2, 3]) # 注意添加的顺序,我们是从左边开始添加的,先添加"我",然后"屮"跑到开头就把"我"挤到右边了,所以是结果是倒过来的 # 那么如果消费deque里面的元素呢? print(d.pop()) # 3 print(d.pop()) # 2 print(d.pop()) # 1 print(d.pop()) # x print(d.popleft()) # 茻 # pop是从右端删除一个元素,popleft是从左端开始删除一个元素。但是如果我想pop掉指定的索引的元素,只能用pop函数,传入索引值即可 # 注意:deque和queue一样,是线程安全的,是受GIL这把超级大锁保护的,可以不同的线程中进行消费。 # 如果想清空里面的元素的话,可以像list、dict一样,使用clear函数 d.clear() print(d) # deque([]) # 旋转 # deque还有一个很用的地方就是可以按任意一个方向进行旋转,从而跳过某些元素。 # d.rotate(n)-->n大于0,从右边开始取n个元素放到左边,n小于0,从左边取n个元素放到右边 d = deque(range(10)) print(d) # deque([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) d.rotate(2) # 从右边取2个元素放到左边,所以8和9被放到了左边 print(d) # deque([8, 9, 0, 1, 2, 3, 4, 5, 6, 7]) d.rotate(-3) # 从左边取3个元素放到右边,所以8、9、0被放到了右边 print(d) # deque([1, 2, 3, 4, 5, 6, 7, 8, 9, 0]) # 限制队列的大小 # 我们在初始化一个双端队列的时候,还可以限制它的大小 d = deque("abcdefg", maxlen=5) # 我们初始化7个元素,但是指定最大长度只有5,所以前面两个("a"和"b")就被挤出去了 print(d) # deque(['c', 'd', 'e', 'f', 'g'], maxlen=5) d.appendleft("yoyoyo") # 当我往前面添加元素的时候,后面的就被挤出去了,因为队列最多只能容纳5个元素 print(d) # deque(['yoyoyo', 'c', 'd', 'e', 'f'], maxlen=5)
5.namedtuple:带名字字段的元组子类
from collections import namedtuple # 传入名字,和字段 person = namedtuple("person", ["name", "age", "gender"]) person1 = person(name="mashiro", age=16, gender="f") print(person1) # person(name='mashiro', age=16, gender='f') print(person1.name, person1.age, person1.gender) # mashiro 16 f print(person1[0]) # mashiro ''' 可以看到不仅可以像普通的tuple一样使用索引访问,还可以使用像类一样通过.字段名访问 ''' person2 = person("satori", 16, "f") print(person2) # person(name='satori', age=16, gender='f') ''' 注意:这个和普通的元组一样,是不可以修改的 ''' try: person2.name = "xxx" except AttributeError as e: print(e) # can't set attribute # 非法字段名,不能使用Python的关键字 try: girl = namedtuple("女孩们", ["for", "in"]) except ValueError as e: print(e) # Type names and field names cannot be a keyword: 'for' # 如果字段名重复了怎么办 try: girl = namedtuple("女孩们", ["name", "age", "age"]) except ValueError as e: print(e) # Encountered duplicate field name: 'age' # 如果非要加上重名字段呢,可以设置一个参数 girl = namedtuple("女孩们", ["name", "age", "age"], rename=True) print(girl) # <class '__main__.女孩们'> girl1 = girl("koishi", 15, 15) # 可以看到重复的字段名会按照索引的值,在前面加上一个下划线。比如第二个age重复,它的索引是多少呢?是2,所以默认帮我们把字段名修改为_2 print(girl1) # 女孩们(name='koishi', age=15, _2=15) # 此外我们所有的字段名都保存在_fields属性中 print(girl1._fields) # ('name', 'age', '_2')
6.OrderDict:记住字典增加键的顺序
from collections import OrderedDict ''' OrderDict是一个字典子类,可以记住字典中增加键的顺序。 在Python2中,字典是无序的,但在Python3中,字典默认是有序的 ''' d = OrderedDict() d["a"] = "A" d["b"] = "B" d["c"] = "C" for k, v in d.items(): print(k, v) ''' a A b B c C ''' # 此外也可以在初始化的时候,添加元素 print(OrderedDict({"a": 1})) # OrderedDict([('a', 1)]) # 相等性,对于常规字典来说,只要里面元素一样便是相等的,不考虑顺序。但是对于OrderDict来说,除了元素,顺序也要一样,否则就不相等 d1 = {"a": 1, "b": 2} d2 = {"b": 2, "a": 1} print(d1 == d2) # True d1 = OrderedDict({"a": 1, "b": 2}) d2 = OrderedDict({"b": 2, "a": 1}) print(d1 == d2) # False # 重排 # 在OrderDict中可以使用move_to_end()将键移至序列的起始位置或末尾位置来改变键的顺序 d3 = OrderedDict({"a": 1, "b": 2, "c": 3, "d": 4}) d3.move_to_end("c") # 表示将key="c"的这个键值对移动到末尾 print(d3) # OrderedDict([('a', 1), ('b', 2), ('d', 4), ('c', 3)]) d3.move_to_end("c", last=False) # 表示将key="c"的这个键值对移动到行首 print(d3) # OrderedDict([('c', 3), ('a', 1), ('b', 2), ('d', 4)])
7.collections.abc:容器的抽象基类
from collections import abc ''' abc模块包含了一些抽象基类,其为Python内置容器数据结构以及collections模块定义的容器数据结构定义了API。 除了明确地定义不同容器的API,这些抽象基类还可以在调用对象之前用instance()测试一个对象是否支持一个API。 有些类还提供了方法实现,它们可以作为"混入类(mix-in)"构造定制容器,而不必从头实现每一个方法。 '''
(三)数组:固定的数据序列
import array ''' array模块定义了一个序列数据结构,看起来与list很相似,只不过所以成员都必须是相同的数据类型。 支持的类型包括数值类型或其他固定大小的基本类型(比如:字节) array成员的类型代码 代码 类型 最小大小(字节) b Int 1 B INT 1 h Signed short 2 H Unsigned short 2 i Signed int 2 I Unsigned int 2 l Signed long 4 L Unsigned long 4 q Signed long long 8 Q Unsigned long long 8 f Float 4 d Double 8 '''
1.初始化
import array import binascii ''' array被实例初始化时可以提供一个参数来描述允许哪种数据类型,还可以有一个存储在数组中的初始数据序列。 ''' s = b"this is a array" a = array.array("b", s) print("byte string:", s) # byte string: b'this is a array' print("as array:", a) # as array: array('b', [116, 104, 105, 115, 32, 105, 115, 32, 97, 32, 97, 114, 114, 97, 121]) print("as hex:", binascii.hexlify(a)) # as hex: b'746869732069732061206172726179'
2.处理数组
import array ''' 与其他Python序列类似,可以采用同样的方式扩展和处理array ''' a = array.array("i", range(3)) print("初始化:", a) # 初始化: array('i', [0, 1, 2]) a.extend(range(3)) print("扩展:", a) # 扩展: array('i', [0, 1, 2, 0, 1, 2]) print("切片:", a[2: 5]) # 切片: array('i', [2, 0, 1]) print("迭代器:", list(enumerate(a))) # 迭代器: [(0, 0), (1, 1), (2, 2), (3, 0), (4, 1), (5, 2)]
3.数组和文件
import array import binascii import tempfile ''' 可以使用专门的高效读写文件的内置方法将数组的内容写入文件或从文件读出数组 ''' a = array.array("i", range(5)) print("A1:", a) # A1: array('i', [0, 1, 2, 3, 4]) # 将数组的元素写到临时文件当中,那么首先要创建一个临时文件 output = tempfile.TemporaryFile() # tofile函数会把数组a里面的内容写到output这个临时文件当中 a.tofile(output) # 刷新 output.flush() # 读取数据 # 读取内容 output.seek(0) raw_data = output.read() print("raw_content:", binascii.hexlify(raw_data)) # raw_content: b'0000000001000000020000000300000004000000' # 将内容读到数组当中,但是我们刚才读过一遍了,因此别忘了把指针移到文件的开头 output.seek(0) # 创建新数组 a2 = array.array("i") # 调用fromfile把f里面的内容写到数组a2里面去,写入的长度和数组a的长度一样 a2.fromfile(output, len(a)) print("A2:", a2) # A2: array('i', [0, 1, 2, 3, 4]) # 其中tofile使用tobytes格式化数据,fromfile使用frombytes再转化为一个数组实例 a = array.array("i", range(5)) print("a:", a) # a: array('i', [0, 1, 2, 3, 4]) as_bytes = a.tobytes() print("as_bytes:", binascii.hexlify(as_bytes)) # as_bytes: b'0000000001000000020000000300000004000000' a2 = array.array("i") a2.frombytes(as_bytes) print(a2) # array('i', [0, 1, 2, 3, 4])
(四)heapq:堆排序算法
import heapq ''' 堆(heap)是一个树形数据结构,其中子节点与父节点有一种有序关系。 二叉堆(binary heap)可以使用一个有组织的列表或数组表示,其中元素N的子元素位于2*N+1和2*N+2(索引从0开始)。 这种布局允许原地重新组织对,从而不必在增加或删除元素时重新分配大量内存。 最大堆(max-heap)确保父节点大于或等于其两个子节点,最小堆(min-heap)要求父节点小于或等于其子节点。Python的heapq实现了一个最小堆 '''
import heapq ''' 创建堆有两种方式:heappush()和heapify() ''' ''' 最主要我们可以使用heapq来获取序列极值 ''' data = [1, 1, 2, 2, 2, 3, 4, 4, 4, 4, 4, 4, 5, 5, 5, 6, 6] # 从data中选出3个最大的元素 print(heapq.nlargest(3, data)) # [6, 6, 5] # 从data中选出3个最大的元素,按照什么来选,出现的次数来选,也就是选择出现次数最多的三个元素,这里显然都是4 print(heapq.nlargest(3, data, key=lambda x: data.count(x))) # [4, 4, 4] data = {"a": 3, "b": 2, "c": 11, "d": 1, "e": 16} # 选择三个最大的元素,按照value来选择 print(heapq.nlargest(3, data, lambda x: data[x])) # ['e', 'c', 'a'] # 高效合并有序序列 data1 = "abc" data2 = [1, 2, 3] data3 = {4, 5, 6} data4 = {"k": 1, "m": 2} from itertools import chain data = chain(data1, data2, data3, data4) # 得到的data是一个迭代器 print(list(data)) # ['a', 'b', 'c', 1, 2, 3, 4, 5, 6, 'k', 'm']
(五)bisect:维护有序的列表
import bisect ''' bisect模块实现了一个算法来向列表中插入元素,同时仍然保证列表有序 '''
1.有序插入
import bisect ''' 可以使用bisect.insort向一个列表中插入元素 ''' values = [46, 39, 80, 91, 62, 71, 32, 70, 15, 6] # 比如我此刻往values里面插入50这个元素 bisect.insort(values, 50) print(values) # [46, 39, 50, 80, 91, 62, 71, 32, 70, 15, 6] ''' bisect默认是从小到大的,因此46比50小,39比50小,80比50大,因此插在了39和80之间 至于我们的values本身就是无序的,因此不可能会把values变成有序的。只能找一个左边比要插入的值小,右边比要插入的值大,然后在这个位置把值插进去 ''' # 我们新建一个列表 l = [] for i in values: bisect.insort(l, i) print("-"*20) print(f"{i} {l}") ''' -------------------- 46 [46] -------------------- 39 [39, 46] -------------------- 50 [39, 46, 50] -------------------- 80 [39, 46, 50, 80] -------------------- 91 [39, 46, 50, 80, 91] -------------------- 62 [39, 46, 50, 62, 80, 91] -------------------- 71 [39, 46, 50, 62, 71, 80, 91] -------------------- 32 [32, 39, 46, 50, 62, 71, 80, 91] -------------------- 70 [32, 39, 46, 50, 62, 70, 71, 80, 91] -------------------- 15 [15, 32, 39, 46, 50, 62, 70, 71, 80, 91] -------------------- 6 [6, 15, 32, 39, 46, 50, 62, 70, 71, 80, 91] ''' ''' 这是一个很简单的例子,实际上,对于这种数据量来说,直接构建列表完成完成一次排序可能速度会更快。 不过对于长列表而言,使用类似这样一个插入排序算法可以大大节省时间和内存。 所以bisect维护的是一个有序的序列,如果一个序列已经有序了,我们使用这种方法插入值,比直接插入值再sort的效率更高 '''
2.处理重复
import bisect ''' 如果插入一个序列中已经出现的值,该怎么办呢?是插在左边呢,还是插在右边呢? bisect提供了两种做法,bisect.insort_right和bisect.insort_left,从名字也能看出来。 至于bisect.insort默认等于bisect.insort_right,是插在右边的。 这两种方法没有什么区别 ''' # 不过bisect还有一种用法 values = [1, 3, 5, 7, 11] # 首先我想插入一个元素7,可以使用bisect.insort,但是如果我想获取7这个元素该插在哪个位置,该怎么办呢? # 可以使用bisect.bisect print(bisect.bisect(values, 7)) # 4 ''' 结果为4,表示我应该插在索引为4的这个地方,原来索引为4的地方是11 所以当7插进去之后,那么11被挤到了后面,因此正好保证有序 同理还有bisect.bisect_left ''' print(bisect.bisect_left(values, 7)) # 3 ''' 得到的结果为3,至于原因,我想不需要再赘述了 bisect.bisect也等价于bisect.bisect_right '''
(六)queue:线程安全的FIFO(先进先出)实现
import queue ''' queue提供了适用于多线程编程的先进先出(FIFO, first-in, first-out)数据结构,可以用来在生产者和消费者线程之间安全地传递消息或其他数据 它会为调用者处理锁定,使多个线程能够安全且容易地处理同一个Queue实例。因为这也是受GIL全局锁保护的 Queue的大小(可以容纳的元素个数)可能首先,以限制内存使用或处理 '''
1.基本的FIFO队列
import queue ''' Queue类实现了一个基本的先进先出的容器。使用put将元素增加到这个序列的一端,使用get从另一端获取。 想象一个管子,两边都有口,我从一端塞进去,然后从另一端获取,先塞进去的会先跑到另一端 比如我从左边塞进去1 2 3 4这个四个元素,1先塞进去的话,那么1肯定在最右边,那么我从另一边最先获取的也是1 ------------- 4 3 2 1 ------------- 但如果是栈的话,由于只有一个口,右边被堵死了,所以即便我从左边塞进去,我还是要从左边获取。因此最先放进去的,要最后才能出来 -------------· | 4 3 2 1 | | -------------· ''' # 调用Queue这个类实例化一个队列 q = queue.Queue() for i in range(5): # 调用put方法塞进去 q.put(i) # 可以查看属性 # 这里显示为0是因为我们没有指定最大容量 print(q.maxsize) # 0 # 我们没有指定容量,所以默认无限大,只要你内存足够大。 print(q.full()) # False # 判断队列是否为空 print(q.empty()) # False # 查看队列的长度,也就是看看队列的肚子里面坏了几个孩子(装了几个元素) # 注意不可以使用len(q)来判断,因为Queue这个类没有实现__len__方法 print(q.qsize()) # 5 while not q.empty(): # 可以看到实现了先进先出,这与我们插进去的顺序是一样的 print(q.get(), end=" ") # 0 1 2 3 4 # 另外解释一下这个get里面的参数 ''' def get(self, block=True, timeout=None): 这个block,表示是否阻塞,默认为True,表示如果获取不到元素,就会一直卡在这个地方,直到队列里面有数据为止。 如果为False,表示不阻塞,队列为空获取不到元素的时候会立即报错,这个一般是在多线程当中会使用 timeout表示超时时间,这个参数有什么用呢? 比如消费者消费数据,但是生产者还没生产完,所以消费者会卡住,需要生产者生产完数据放到队列里面去,消费者才能获取数据继续往下走 如果生产者不生产了,消费者卡住。设置block=False的话呢,可以避免,但是有可能生产者生产的比较慢,不是说不生产了,这种情况会导致生产者还没把数据放进去的时候就已经报错了 所以timeout的作用就来了,表示消费者获取不到数据的时候,依旧会卡住,但是不会一直卡住,会保持一段时间,如果时间过后还没有数据的话再报错。 所以要搭配block=True来使用,如果block=False的话,这个参数就没有什么意义了,因为获取不到直接报错,就没有timeout什么事了。同理对于put也是一样,如果数据满了,放不进去了,put也提供了block和timeout参数供我们使用 '''
2.LIFO队列
import queue ''' 与Queue的标准FIFO实现相反,LifoQueue使用了(通常与栈数据结构关联的)后进先出(LIFO)顺序 ''' q = queue.LifoQueue() for i in range(5): q.put(i) while not q.empty(): print(q.get(), end=" ") # 4 3 2 1 0 # 可以看到,这个和栈比较类似,是先入后出的
3.优先队列
import queue import functools import threading ''' 有些情况下,需要根据队列中元素的特性来决定这些元素的处理顺序,而不是简单地采用队列中创建或插入元素的顺序。 例如,工资部门的打印作业可能就优先于某个开发人员要想打印的代码清单。PriorityQueue使用队列内容的有序顺序来决定获取哪一个元素 ''' @functools.total_ordering class Job: def __init__(self, priority, description): self.priority = priority self.description = description print("New Job:", description) def __eq__(self, other): try: return self.priority == other.priority except AttributeError: return NotImplemented def __lt__(self, other): try: return self.priority < other.priority except AttributeError: return NotImplemented q = queue.PriorityQueue() q.put(Job(3, "优先级第3")) q.put(Job(10, "优先级第10")) q.put(Job(1, "优先级第1")) def process_job(q): while q.qsize() > 0: next_job = q.get() print("processing job", next_job.description) q.task_done() process_job(q) workers = [ threading.Thread(target=process_job, args=(q, )), threading.Thread(target=process_job, args=(q, )) ] for w in workers: w.start() ''' 这个的q.join()和上面的q.task_done()有必要说一下。 首先队列中有这么一个属性,在Queue这个类的构造函数中:self.unfinished_tasks = 0 这个表示什么含义呢,表示未完成的任务,也就是说我们每put一次,这个计数就会加上1 def put(self, item, block=True, timeout=None): 省略。。。 self._put(item) self.unfinished_tasks += 1 self.not_empty.notify() 使用join的话,是等这个计数变为0才能往下走,否则就会卡住。 因此在我们取出一个元素的时候,要记得调用一下q.task_done()来让计数减1 def task_done(self): with self.all_tasks_done: unfinished = self.unfinished_tasks - 1 if unfinished <= 0: ''' q.join() # 输出结果 ''' New Job: 优先级第3 New Job: 优先级第10 New Job: 优先级第1 processing job 优先级第1 processing job 优先级第3 processing job 优先级第10 '''# 可以看到会顺序打印,为什么?那是因为优先级队列会自动排序,而我们定义好了排序规则 ''' 这个例子有多个线程在处理作业,要根据get()时队列中元素的优先级来处理。 运行消费者线程时,增加到队列的元素的处理顺序取决于线程上下文切换。 '''
4.构建一个多线程播客客户端程序
from queue import Queue ''' 现在我们将构建一个播客客户端程序,程序的代码展示了如何利用多个线程使用Queue类。 这个程序要读入一个或多个rss提要,对每一个提要的专辑排队,显示最新的五集以供下载,并使用多线程并发处理多个下载。 这里没有提供完备的错误处理,所以不能在实际的生产环境中使用,不过这个框架可以作为很好的例子来说明如何使用queue模块。 首先要建立一些操作参数,一般情况下,这些参数都来自用户输入(例如,首选项,数据库等)。 不过在这个例子中,线程数和要获取的url列表都采用了硬编码值 ''' import threading import requests from urllib.parse import urlparse import feedparser # 用来解析rss文本的一个模块 # 设置一些全局变量 num_fetch_threads = 2 # 线程数 enclosure_queue = Queue() feed_urls = [ "https://talkpython.fm/episodes/rss" ] def message(s): print(f"{threading.current_thread().name}: {s}") # 函数download_enclosure在工作线程中运行,使用urllib处理下载 def download_enclosures(q): ''' 这是一个工作线程函数,一个接一个地处理每一个成员。 而这些守护线程会进入一个无限循环,只有当主线程结束时才会推出 :param q: :return: ''' while True: message("寻找下一个闭包") url = q.get() # 获取url最后一个/右面的内容当做文件名 filename = url.rpartition("/")[-1] # 下载文件 message(f"downloading {filename}") # 下载内容 response = requests.get(url) data = response.content # 保存内容到filename当中 with open(filename, "wb") as f: f.write(data) # 不要忘记处理完了之后,要task_done q.task_done() ''' 一旦定义了线程的目标函数,接下来便可以启动工作线程。 download_enclosures处理语句url=q.get()时,会阻塞并等待,直到队列返回某个结果。 这说明,即使队列中没有任何内容,也可以安全地启动线程 ''' # 启动一些线程 for i in range(num_fetch_threads): worker = threading.Thread( target=download_enclosures, args=(enclosure_queue,), name=f"worker-{i}" ) worker.setDaemon(True) worker.start() ''' 下一步使用feedparser模块获取提要内容,并将这些专辑的url入队。 一旦第一个url增加到队列,就会有某个工作线程提取这个url,并且还是下载。 这个循环会继续增加元素,直到这个提要已经被完全消费,工作线程会依次将url出队以完成下载 ''' for url in feed_urls: response = feedparser.parse(url) for entry in response["entries"][: 5]: for enclosure in entry.get("enclosures", []): parsed_url = urlparse(enclosure["url"]) message(f"queuing {parsed_url.path.rpartition('/')[-1]}") enclosure_queue.put(enclosure['url']) # 现在我们要使用join等待队列为空 message("***main thread waiting") enclosure_queue.join() message("***done") # 输出内容不再显示
(七)struct:二进制数据结构
import struct ''' struct模块包括一些函数,这些函数可以完成字节串与原生Python数据类型(如数字和字符串)之间的转换 '''
1.函数与struct类
import struct ''' struct提供了一组处理结构值的模块级函数,另外还有一个Struct类。 格式指示符有字符串格式转换为一种编译表示,这与处理正则表达式的方法类似。 这个转换会耗费一些资源,所以创建一个Struct实例并在这个实例上调用方法时(不使用模块级函数)只完成一次转换,这会更高效 类比正则: re.match(pattern, text) 使用这种模块级别的函数时,会先将pattern进行编译转换,这个转换是耗费资源的 因此可以先对pattern进行一个编译,comp = re.compile(pattern) comp.match(text) 这样的话就只需要转换一次,struct也是类似的情况 '''
2.打包与解包
import struct ''' Struct支持使用格式指示符将数据打包(packing)为字符串,另外支持从字符串解包(unpacking)数据。 格式指示符由表示数据类型的字符和可选的数量及字节序(endianness)指示符构成。 要全面了解目前可支持的数据结构,可以参考标准库文档 ''' import binascii # values包含一个整型或长整型,一个两字节字符串,以及一个浮点数。 values = (1, "ab".encode("utf-8"), 2.7) # 格式指示符中包含的空格用来分割类型指示符,并且在编译格式时会被忽略 # 使用Struct定义格式,I:整型,2s:两个字节的字符,f:浮点数,之间使用空格分隔 # 表示打包的数据有三个,分别是整型,两个字节的字符,以及一个浮点 s = struct.Struct("I 2s f") # 使用s.pack函数进行打包,将values打开传进去 packed_data = s.pack(*values) # s:Struct对象 print(s) # <Struct object at 0x0000000002924458> # 原始数据values print("原始数据:", values) # 原始数据: (1, b'ab', 2.7) # 打印一下我们的格式,也就是我们传进去的格式 print("格式化字符:", s.format) # 格式化字符: I 2s f # 查看所用的字节 print("使用:", s.size, "bytes") # 使用: 12 bytes # 查看打包之后的结果 print("打包后的结果:", packed_data) # 打包后的结果: b'\x01\x00\x00\x00ab\x00\x00\xcd\xcc,@' print("将打包的结果进行转换:", binascii.hexlify(packed_data)) # 将打包的结果进行转换: b'0100000061620000cdcc2c40' # 我们传入values,通过s.pack()得到packed_data,那么我们传入packed_data,可不可以调用一个函数反过来得到values呢? # 答案是可以的,可以使用s.unpack() # 值得一提的是,这个binascii.hexlify,还有一个相反的函数叫做binascii.unhexlify print(packed_data) # b'\x01\x00\x00\x00ab\x00\x00\xcd\xcc,@' print(binascii.hexlify(packed_data)) # b'0100000061620000cdcc2c40' print(binascii.unhexlify(binascii.hexlify(packed_data))) # b'\x01\x00\x00\x00ab\x00\x00\xcd\xcc,@' # 使用s.unpack() print(s.unpack(packed_data)) # (1, b'ab', 2.700000047683716) ''' 可以看到还是可以转回来的,注意这个浮点数啊,这是计算机的存储误差,任何语言都是有这个问题的。 '''
3.字节序
import struct ''' 默认地,值会使用原生C库的字节序(endianness)来编码。 只需在格式中提供一个显示的字节序指令,就可以很容易地覆盖这个默认选择 ''' import binascii values = (1, "ab".encode("utf-8"), 2.7) print("original values:", values) endianness = [ ("@", "native, native"), ("=", "native, standard"), ("<", "little-endian"), (">", "big-endian"), ("!", "network") ] for code, name in endianness: s = struct.Struct(code + " I 2s f") packed_data = s.pack(*values) print("*"*20) print("Format string: ", s.format, "for", name) print("uses: ", s.size, "bytes") print("hex packed data:", binascii.hexlify(packed_data)) print("unpacked data", s.unpack(packed_data)) # @:原生顺序 # =:原生标准 # <:小端 # >:大端 # !:网络顺序 ''' original values: (1, b'ab', 2.7) ******************** Format string: @ I 2s f for native, native uses: 12 bytes hex packed data: b'0100000061620000cdcc2c40' unpacked data (1, b'ab', 2.700000047683716) ******************** Format string: = I 2s f for native, standard uses: 10 bytes hex packed data: b'010000006162cdcc2c40' unpacked data (1, b'ab', 2.700000047683716) ******************** Format string: < I 2s f for little-endian uses: 10 bytes hex packed data: b'010000006162cdcc2c40' unpacked data (1, b'ab', 2.700000047683716) ******************** Format string: > I 2s f for big-endian uses: 10 bytes hex packed data: b'000000016162402ccccd' unpacked data (1, b'ab', 2.700000047683716) ******************** Format string: ! I 2s f for network uses: 10 bytes hex packed data: b'000000016162402ccccd' unpacked data (1, b'ab', 2.700000047683716) '''
4.缓冲区
import struct ''' 通常在强调性能的情况下,或者向扩展模块传入、传出数据时,才会处理二进制打包数据。 通过避免为每个打包结构分配一个新缓冲区所带来的开销,这些情况可以得到优化。 pack_into和unpack_from方法支持直接写入预分配的缓冲区 ''' import binascii import ctypes import array s = struct.Struct("I 2s f") values = (1, "ab".encode("utf-8"), 2.7) print("original:", values) print("---------------") print("ctypes string buffer") # 创建一个string缓存,大小为s.size b = ctypes.create_string_buffer(s.size) print("before:", b.raw, binascii.hexlify(b.raw)) # s.pack表示打包,s.pack_into表示打包到什么地方,至于第二个参数0表示偏移量,表示从头开始 s.pack_into(b, 0, *values) print("after:", b.raw, binascii.hexlify(b.raw)) # s.unpack表示解包,s.unpack_from表示从什么地方解包,参数0表示偏移量,表示从头开始 print("unpacked:", s.unpack_from(b, 0)) print("---------------") print("array") a = array.array("b", b"\0"*s.size) print("before:", a, binascii.hexlify(a)) s.pack_into(a, 0, *values) print("after:", binascii.hexlify(a)) print("unpacked:", s.unpack_from(a, 0)) ''' original: (1, b'ab', 2.7) --------------- ctypes string buffer before: b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' b'000000000000000000000000' after: b'\x01\x00\x00\x00ab\x00\x00\xcd\xcc,@' b'0100000061620000cdcc2c40' unpacked: (1, b'ab', 2.700000047683716) --------------- array before: array('b', [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) b'000000000000000000000000' after: b'0100000061620000cdcc2c40' unpacked: (1, b'ab', 2.700000047683716) '''
(八)weakref:对象的弱引用
import weakref ''' weakref支持对象的弱引用,正常的引用会增加对象的引用计数,并避免它被垃圾回收。 但结果并不是总和期望的那样,比如有时候可能会出现一个循环引用,或者有时候需要内存时可能要删除对象的缓存。 弱引用(weak reference)是一个不能避免对象被自动清理的对象句柄 '''
1.引用
import weakref ''' 对象的弱引用要通过ref类来管理。要获取原对象,可以调用引用对象 ''' class RefObject: def __del__(self): print("del executed") obj = RefObject() # 创建弱引用 r = weakref.ref(obj) print("obj:", obj) # obj: <__main__.RefObject object at 0x0000000002964470> # 显示关联RefObject print("ref:", r) # ref: <weakref at 0x000000000051BA48; to 'RefObject' at 0x0000000002964470> # 引用r加上(),等价于obj,因此得到RefObject的实例对象 print("ref()", r()) # ref() <__main__.RefObject object at 0x0000000002964470> print("deleting obj") # deleting obj # 删除obj执行析构函数 del obj # del executed # 之前说过调用r()等价于调用obj,但是obj被删除了,所以返回None print("r():", r()) # r(): None
2.引用回调
import weakref ''' ref构造函数可以接受一个可选的回调函数,删除引用所指向的对象时就会调用这个回调函数 ''' class RefObject: def __del__(self): print("del executed") def callback(reference): print(f"callback : {reference}") obj = RefObject() r = weakref.ref(obj, callback) ''' 当引用已经"死亡"而且不再引用原对象时,这个回调会接受这个引用对象作为参数。 这种特性的一种用法就是从缓存中删除弱引用对象。 ''' print("obj:", obj) print("ref:", r) print("ref()", r()) print("deleting obj") del obj print("r():", r()) ''' obj: <__main__.RefObject object at 0x0000000002964630> ref: <weakref at 0x0000000001D2BA48; to 'RefObject' at 0x0000000002964630> ref() <__main__.RefObject object at 0x0000000002964630> deleting obj del executed callback : <weakref at 0x0000000001D2BA48; dead> # 删除obj,执行回调,显示dead r(): None '''
3.最终化对象
import weakref ''' 清理若对象引用时要对资源完成更健壮的管理,可以使用finalize将回调与对象关联。 finalize实例会一直保留(直到所关联的对象被删除),即使没有保留最终化对象的引用 ''' class RefObj: def __del__(self): print("xxx") def on_finalize(*args): print(f"on_finalize: {args}") obj = RefObj() weakref.finalize(obj, on_finalize, "callback的参数") del obj ''' xxx on_finalize: ('callback的参数',) ''' # finalize的参数包括要跟踪的对象,对象被垃圾回收时要调用的callback,以及参数(可以是位置参数,也可以是关键字参数) # finalize实例对象还有一个atexit属性,用来控制程序退出时是否调用这个回调(如果还未调用) obj1 = RefObj() f = weakref.finalize(obj1, on_finalize, "callback的参数") # 默认是调用回调,但是将atexit设置为False会禁用这种行为 f.atexit = False ''' 不会有任何的输出,注意:这里我没有显示的删除obj1. 因为在f.atexit=True的情况下,即使不删除也依旧会执行callback,执行完callback会触发析构函数 '''
import weakref ''' 如果向finalize实例提供一个跟踪对象的引用,这便会导致一个引用被保留,所以这个对象永远不会被垃圾回收 ''' class RefObj: def __del__(self): print("xxx") def on_finalize(*args): print(f"on_finalize: {args}") obj = RefObj() obj_id = id(obj) f = weakref.finalize(obj, on_finalize, obj) f.atexit = False del obj import gc for o in gc.get_objects(): if id(o) == obj_id: print("found uncollected object in gc") # found uncollected object in gc
4.代理
import weakref ''' 有时候使用代理比使用弱引用更方便。使用代理可以像使用原对象一样,而且不要求在访问对象之前先调用代理。 这说明,可以将代理传递到一个库,而这个库并不知道它接收的是一个引用而不是真正的对象。 ''' class RefObj: def __init__(self, name): self.name = name def __del__(self): print("xxx") obj = RefObj("my obj") r = weakref.ref(obj) p = weakref.proxy(obj) print("via obj:", obj.name) # via obj: my obj print("via ref:", r().name) # via ref: my obj print("via proxy:", p.name) # via proxy: my obj del obj # xxx try: # 删除对象之后,再调用引用,打印为None print(r()) # None # 但是如果调用代理的话,则会抛出一个ReferenceError print(p) except Exception as e: print(e) # weakly-referenced object no longer exists
(九)copy:复制对象
import copy ''' copy模块包括两个函数copy()和deepcopy(),用于复制现有的对象 '''
1.浅拷贝
import copy ''' copy()创建的浅副本(shallow copy)是一个新容器,其中填充了原对象内容的引用。 ''' l1 = [1] l2 = copy.copy(l1) print(l1[0] is l2[0]) # True ''' 可以看到只是拷贝了引用 ''' l2[0] = 111 print(l1) # [1] print(l2) # [111] ''' 当我们对l2进行修改的时候,由于是重新赋值,所以只会改变l2,而不会改变l1 ''' # 但是,此时l1内部存放一个列表,列表是可变的对象,可以在本地修改或添加内容 l1 = [[]] l2 = copy.copy(l1) l2[0].append("xxx") print(l1) # [['xxx']] print(l2) # [['xxx']] ''' 可以看到由于只是拷贝了引用(或者说是地址,指针),因此两者指向同一片内存。 如果是赋值,那么会指向别处。但是列表是可变类型,是支持在原处修改的,因此修改任意一个都会影响另一个 ''' # 对于列表来说,l2 = l1[:],与浅拷贝是等价的 l1 = [[]] l2 = l1[:] l1[0].append(0) print(l1) # [[0]] print(l2) # [[0]]
2.深拷贝
import copy ''' 浅拷贝则是是拷贝引用,而深拷贝则是拷贝内存里存储的值 ''' class A: def __init__(self, li): self.li = [] a = A([]) a1 = copy.copy(a) a1.li.append("xxx") print(a.li) # ['xxx'] # 深拷贝 a2 = copy.deepcopy(a) a2.li.remove("xxx") print(a.li) # ['xxx'] ''' 可以看到我对a2里的li,执行remove操作并没有影响a。 ''' l1 = [[]] l2 = l1[:] l2[0].append("111") print(l1) # [['111']] l3 = copy.deepcopy(l1) l3[0].append("222") print(l1) # [['111']] ''' 可以看到对l3进行操作,不会影响l1。因为深拷贝是拷贝值,所以l3操作的是自己的内存,不会影响l1 ''' print(id(l1[0])) # 31458440 print(id(l2[0])) # 31458440 print(id(l3[0])) # 31503688 ''' 所以l3[0]的引用也发生变化了,因为引用不再指向原来的内存,而是指向新的内存 '''
(十)pprint:美观打印数据结构
from pprint import pprint ''' pprint模块包含一个美观打印机,用于生成数据结构的一个美观的视图。格式化工具会生成数据结构的一些表示, 不仅能够由解释器正确的解析,还便于人阅读。输出会尽可能放在一行上,分解为多行时会缩进 '''
1.打印
from pprint import pprint from string import ascii_letters import random # 生成一堆随机数据 d = {random.randint(1, 9): random.sample(list(ascii_letters), 3) for i in range(1, 20)} # 调用pprint函数,美观打印。可以添加一个stream参数,默认是打印到sys.stdout,也就是控制台 pprint(d) ''' {1: ['O', 'W', 'F'], 2: ['s', 'B', 'I'], 3: ['Z', 'O', 'z'], 4: ['g', 'o', 'w'], 5: ['V', 'D', 'b'], 6: ['l', 'z', 'q'], 7: ['c', 'f', 'C'], 8: ['l', 'E', 'u'], 9: ['D', 'e', 'C']} ''' # 相比打印在一行,这样是不是很美观呢?
2.格式化
from pprint import pformat from string import ascii_letters import random ''' 如果我们想美观地把数据写到某一个地方中,该怎么办呢? 我们使用pprint可以美观地打印到控制台当中,但我如果不想打印,而是存到某一个地方,或者赋值给某个变量呢? 可以使用pformat,美观格式化 ''' # 生成一堆随机数据 d = {random.randint(1, 9): random.sample(list(ascii_letters), 3) for i in range(1, 20)} p = pformat(d) print(p) ''' {1: ['g', 'R', 'a'], 2: ['O', 'k', 'M'], 3: ['y', 'u', 'I'], 4: ['V', 'E', 'P'], 5: ['c', 'e', 'h'], 6: ['B', 'i', 'U'], 7: ['w', 'z', 'K'], 8: ['x', 'z', 'q'], 9: ['I', 'B', 'W']} '''