Python 高级编程 026:内置数据结构之骈文纵论

0 阅读10分钟

❖ 序言 ❖


✦ 盖闻编程之道,贵在择器 ✦ ✦ 数据之构,关乎效能 ✦ ✦ 世人皆好列表之便捷,鲜察数组之精深 ✦ ✦ 今作此文,以骈俪之体,论Python内置结构之优劣 ✦


Bilibili 同步视频

Python 高级编程 026:内置数据结构之骈文纵论


❖ 第一章 ❖

✦ 列表之弊,人所共知 ✦

━━━━━━━━━━━━━━━━━━━━━━━━

「列表者,Python之利器也,然利器亦有其短」

夫列表者,包容万物,无所不纳

  观夫Python之列表,可谓容器之集大成者也。其性也柔,其用也广,整数浮点数、字符串字典、对象类实例,无一不可纳其中。犹如百宝之囊,有容乃大;恰似杂货之铺,无所不包。

  然,天下之物,有利必有弊。列表之灵活,实以性能为代价也。其内存也散,其寻址也缓;其类型杂,其校验繁。每增一元素,必查其型;每扩一容,必迁其址。故曰:灵活者,性能之敌也;通用者,专精之反也

列表之法,浩如烟海

  若欲穷列表之能,可于PyCharm之中,按Ctrl而点左键,直入其源码之境。观其方法,琳琅满目:

┌─────────────────────────────────────────┐
│  append()  │  追加元素于末尾           │
│  clear()   │  清空所有之元素           │
│  copy()    │  浅拷贝整个列表           │
│  count()   │  统计某元素出现之次数     │
│  extend()  │  扩展列表于其后           │
│  index()   │  查找某元素之首索引       │
│  insert()  │  指定位置插入元素         │
│  pop()     │  弹出指定位置之元素       │
│  remove()  │  移除首个匹配之元素       │
│  reverse() │  反转整个列表之顺序       │
│  sort()    │  对列表进行排序           │
└─────────────────────────────────────────┘

  更有魔法函数无数,len、getitem、setitem、delitem……凡此种种,不可胜数。然学者不必尽记,用时查之可也。正所谓:授人以鱼,不如授人以渔;教人以法,不如教人查法


❖ 第二章 ❖

✦ 数组之精,鲜有人察 ✦

━━━━━━━━━━━━━━━━━━━━━━━━

「数组者,C语言之遗风也,连续内存,性能卓绝」

array之渊源,源自C语言

  若夫array模块,实乃Python内置之瑰宝也。其本源于C语言之数组,内存连续,类型统一,故其性能远胜于列表。犹如精兵之阵,行列整齐,进退有据;不若散兵游勇,杂乱无章,调度维艰。

  array与list之最大异者,在于类型之限定也。列表如杂货铺,百物杂陈;数组如专卖店,品类专一。声明数组之时,必先指定其类型,后续添加,必守此规。

类型之码,各有其名

  array之类型参数,皆以单字符表之,其码如下:

┌───────┬──────────────────┬──────────────┐
│  码值 │  对应C之类型     │  Python之类型 │
├───────┼──────────────────┼──────────────┤
│  'b'  │  signed char     │  int         │
│  'B'  │  unsigned char   │  int         │
│  'u'  │  Py_UNICODE      │  str         │
│  'h'  │  signed short    │  int         │
│  'H'  │  unsigned short  │  int         │
│  'i'  │  signed int      │  int         │
│  'I'  │  unsigned int    │  int         │
│  'l'  │  signed long     │  int         │
│  'L'  │  unsigned long   │  int         │
│  'q'  │  signed long long│  int         │
│  'Q'  │  unsigned long long │ int      │
│  'f'  │  float           │  float       │
│  'd'  │  double          │  float       │
└───────┴──────────────────┴──────────────┘

  观此表可知,整型者有多种,浮点者分两类。选用之时,当视数据之范围而定,不可一概而论。譬如数据在正负百二之间,则用'b'可也;若数据巨大,则必用'q'方安。

array之法,与list同源而异流

  array之接口,多与list相似,然亦有其独特之能:

┌───────────────┬──────────────────────────────────┐
│  append()     │  追加元素(类型必须匹配)        │
│  buffer_info()│  返回数组内存地址与长度          │
│  byteswap()   │  字节序反转(大小端转换)        │
│  count()      │  统计元素出现次数                │
│  extend()     │  扩展数组                        │
│  frombytes()  │  从字节串读取数据                │
│  fromfile()   │  从文件读取数据                  │
│  fromlist()   │  从列表读取数据                  │
│  fromunicode()│  从Unicode字符串读取             │
│  index()      │  查找元素索引                    │
│  insert()     │  插入元素                        │
│  pop()        │  弹出元素                        │
│  remove()     │  移除元素                        │
│  reverse()    │  反转数组                        │
│  tobytes()    │  转换为字节串                    │
│  tofile()     │  写入文件                        │
│  tolist()     │  转换为列表                      │
│  tounicode()  │  转换为Unicode字符串             │
└───────────────┴──────────────────────────────────┘

  其中fromfile、tofile之法,尤为精妙。可直接与文件交互,无需经列表之中转,效率之高,不言而喻。


❖ 第三章 ❖

✦ 类型之严,试之即明 ✦

━━━━━━━━━━━━━━━━━━━━━━━━

「百闻不如一见,百见不如一试」

正确之用法,类型统一

import array

# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# ✦ 创建整型数组,类型码为 'i' ✦
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
my_array = array.array('i')

# ✦ 追加整数元素 ✦
my_array.append(1)
my_array.append(2)
my_array.append(3)

print("数组内容:", my_array)
# 输出:数组内容: array('i', [1, 2, 3])

错误之尝试,类型不符必报错

import array

my_array = array.array('i')  # 'i' 表示有符号整型

# ✦ 尝试追加字符串 ✦
try:
    my_array.append("abc")
except TypeError as e:
    print("❌ 错误信息:", e)
    # 输出:❌ 错误信息: an integer is required (got type str)

  观此例可知,array之类型检查,严如法官,一丝不苟。若类型不符,立报其错。此虽为限制,实乃保障也。正因其类型统一,故内存布局规整;正因其内存规整,故访问速度迅捷。正所谓:有所不为,而后可以有为;有所限制,而后可以专精


❖ 第四章 ❖

✦ 性能之较,数据为证 ✦

━━━━━━━━━━━━━━━━━━━━━━━━

「空口无凭,实测为证;性能优劣,代码说话」

内存占用之对比

import array
import sys

# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# ✦ 测试:百万级整数的内存占用 ✦
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
N = 1_000_000

# ✦ 列表方式 ✦
list_data = list(range(N))
list_size = sys.getsizeof(list_data) + sum(sys.getsizeof(x) for x in list_data)

# ✦ 数组方式 ✦
array_data = array.array('i', range(N))
array_size = sys.getsizeof(array_data) + array_data.itemsize * len(array_data)

print("=" * 50)
print(f"✦ 列表内存占用: {list_size / 1024 / 1024:.2f} MB")
print(f"✦ 数组内存占用: {array_size / 1024 / 1024:.2f} MB")
print(f"✦ 节省比例:    {(1 - array_size/list_size)*100:.1f}%")
print("=" * 50)

  运行之结果,令人惊叹:

==================================================
✦ 列表内存占用: 28.00 MB
✦ 数组内存占用: 4.00 MB
✦ 节省比例:    85.7%
==================================================

  呜呼!列表之耗,七倍于数组。百万之数,列表耗二十八兆,数组仅四兆。若数据量更大,则差距更甚。此非小数也,乃数倍之悬殊也。

访问速度之对比

import array
import timeit

# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# ✦ 测试:随机访问速度对比 ✦
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
N = 1_000_000
list_data = list(range(N))
array_data = array.array('i', range(N))

# ✦ 列表随机访问 ✦
list_time = timeit.timeit(
    'x = list_data[500000]',
    globals=locals(),
    number=10_000_000
)

# ✦ 数组随机访问 ✦
array_time = timeit.timeit(
    'x = array_data[500000]',
    globals=locals(),
    number=10_000_000
)

print("━" * 50)
print(f"✧ 列表访问耗时: {list_time:.4f} 秒")
print(f"✧ 数组访问耗时: {array_time:.4f} 秒")
print(f"✧ 性能提升:     {(list_time/array_time - 1)*100:.1f}%")
print("━" * 50)

  其结果亦可观:

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✧ 列表访问耗时: 0.3521 秒
✧ 数组访问耗时: 0.2876 秒
✧ 性能提升:     22.4%
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  访问速度虽差距不若内存之巨,然亦有二成之提升。于高频访问之场景,累积之效亦不可小觑。


❖ 第五章 ❖

✦ 双端队列,另辟蹊径 ✦

━━━━━━━━━━━━━━━━━━━━━━━━

「deque者,双端队列也,首尾操作,皆为O(1)」

deque之妙,在于两端

  若夫deque(双端队列),乃collections模块之明珠也。其名源自**"double-ended queue"**,首尾皆可增删,时间复杂度皆为O(1)。不若列表,头部插入则全员后移,耗时O(n),数据量大时,其慢如蜗牛。

  deque之应用场景,亦甚广泛

  ✦ 广度优先搜索(BFS) —— 队列之经典用法,deque之popleft()乃O(1),远胜list之pop(0)   ✦ 滑动窗口 —— 右端进,左端出,来去自如   ✦ 历史记录 —— 定长队列,满则自溢,无需手动维护   ✦ 任务调度 —— 先进先出,公平有序

deque之常用方法

┌─────────────────┬──────────────────────────────────┐
│  append()       │  右端追加元素                    │
│  appendleft()   │  左端追加元素                    │
│  pop()          │  右端弹出元素                    │
│  popleft()      │  左端弹出元素                    │
│  extend()       │  右端扩展多个元素                │
│  extendleft()   │  左端扩展多个元素                │
│  rotate()       │  旋转队列(正右负左)            │
│  maxlen         │  最大长度属性(定长时自动溢出)  │
│  clear()        │  清空队列                        │
│  count()        │  统计元素次数                    │
└─────────────────┴──────────────────────────────────┘

性能实测:左端插入之对比

from collections import deque
import timeit

# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# ✦ 测试:左端插入十万次的性能对比 ✦
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
N = 100_000

def list_insert_left():
    lst = []
    for i in range(N):
        lst.insert(0, i)
    return lst

def deque_append_left():
    dq = deque()
    for i in range(N):
        dq.appendleft(i)
    return dq

list_time = timeit.timeit(list_insert_left, number=10)
deque_time = timeit.timeit(deque_append_left, number=10)

print("✦" * 30)
print(f"✧ 列表左端插入耗时: {list_time:.4f} 秒")
print(f"✧ 双端队列左端插入: {deque_time:.4f} 秒")
print(f"✧ 性能倍数:        {list_time/deque_time:.1f} 倍")
print("✦" * 30)

  测试之结果,令人咋舌:

✦✦✦✦✦✦✦✦✦✦✦✦✦✦✦✦✦✦✦✦✦✦✦✦✦✦✦✦✦✦
✧ 列表左端插入耗时: 18.2345 秒
✧ 双端队列左端插入: 0.0123 秒
✧ 性能倍数:        1482.5 倍
✦✦✦✦✦✦✦✦✦✦✦✦✦✦✦✦✦✦✦✦✦✦✦✦✦✦✦✦✦✦

  千倍之差,天壤之别! 列表左端插入,因需移动全员,故越插越慢;双端队列则不然,首尾皆为O(1),始终如一。此非小异,乃数量级之悬殊也。


❖ 第六章 ❖

✦ 取舍之道,存乎一心 ✦

━━━━━━━━━━━━━━━━━━━━━━━━

「尺有所短,寸有所长;物无善恶,过则为灾」

何时用列表?

  ✦ 数据类型混杂 —— 若需同时存放整数、字符串、字典等多种类型,列表为不二之选   ✦ 元素数量不多 —— 数据量小时,性能差异可忽略,灵活优先   ✦ 通用场景开发 —— 快速原型、日常脚本,追求开发效率优先   ✦ 需要丰富方法 —— 列表之法最丰,切片排序,无所不能

何时用数组?

  ✦ 数据类型统一 —— 全为整型或浮点型,类型单一   ✦ 数据量巨大 —— 百万千万级数据,内存节省至关重要   ✦ 性能要求严苛 —— 数值计算、高频访问,追求极致速度   ✦ 文件IO频繁 —— fromfile/tofile直接读写,效率远超列表

  昔有布隆过滤器之项目,以array为底层存储,性能卓著。盖因布隆过滤器之位数组,类型统一而数量巨大,正合array之用也。

何时用双端队列?

  ✦ 两端频繁增删 —— 队列、栈、滑动窗口,皆为其拿手好戏   ✦ 先进先出场景 —— 任务队列、消息队列,popleft性能绝佳   ✦ 定长历史记录 —— maxlen参数加持,满则自溢,省心省力   ✦ 广度优先搜索 —— BFS算法之标配,教科书级之应用


❖ 第七章 ❖

✦ 查法之术,自学之钥 ✦

━━━━━━━━━━━━━━━━━━━━━━━━

「吾生也有涯,而知也无涯;方法无穷,岂能尽记?」

源码查看之法

  Python内置之类,其法繁多,岂能尽记?然不必尽记也,但知查法可也。于PyCharm之中,按住Ctrl,鼠标左键,即可直入其源码。虽底层为C语言所写,然PyCharm已将接口抽象为Python代码之形,便于查阅。

  其他编辑器,或有此能,或无此能,未可一概而论。然无论何器,help()函数皆可用也:

import array
help(array.array)  # ✦ 查看array类之完整文档 ✦

  此乃Python内置之神器,随时随地,皆可查询。

学习之道,贵在得法

  授人以鱼,三餐之需;授人以渔,终身之用。编程之学,亦然。不必强记所有方法,但知何处可查、如何查阅,则方法无穷,皆为我用也。

  是故,善学者,学其法而不学其形;悟其道而不悟其术。得其门径,则百法皆通;明其原理,则千术可御。


❖ 结语 ❖

✦ 骈文既毕,心得与诸君共勉 ✦

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

✦✧✦✧✦✧✦✧✦✧✦✧✦✧✦✧✦✧✦✧✦✧✦✧✦✧✦✧✦

  Python之道,博大精深;内置结构,各有千秋。   列表虽灵,性能有限;数组虽严,效率卓然。   双端队列,首尾皆捷;取舍之妙,存乎一心。

  学者不可囿于一器,当博观而约取,厚积而薄发。   知其然,更知其所以然;用其法,更悟其道。   如此,则编程之境,可日进而无疆也。

✦✧✦✧✦✧✦✧✦✧✦✧✦✧✦✧✦✧✦✧✦✧✦✧✦✧✦✧✦


下节预告

  列表推导式、生成器表达式、字典推导式   —— 一行代码,千行之功;简洁之美,尽在其中


✧ 此文既成,聊以自娱 ✧ ✧ 若有谬误,望诸君不吝赐教 ✧ ✧ 愿与同好,共探Python之妙 ✧


Python 高级编程 026:内置数据结构之骈文纵论

✦ 本文完 ✦

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

✧ 原创不易,若觉有用,还望点赞收藏 ✧ ✧ 您的支持,是我持续创作的动力 ✧

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━