我自认为自己 Python 学的还行,工作中已经使用它实现了很多功能,直到今天我学习了景霄大神 Python 的课程,才发现,自己学习到的,仅仅是皮毛而已。
我相信很多人也和我一样,编程语言或者其他技术工具会用就行了,几乎不会思考背后的原理和设计哲学。没错,初学者是需要快速学会使用工具或编程语言,这样学会之后可以很快投入使用,为企业提供劳动价值。
仔细思考一下,这也许是大部分人认为的程序员是吃青春饭的一个重要原因。因为学会使用一个工具其实是很容易的,一门新的编程语言,有点计算机基础的,只需完整投入一个星期,基本用法就可以掌握,就可以开干了。但是这么容易学会的东西必然门槛较低,会有大量的新人涌入,他们要求的工资更低,更能加班,作为老员工,你比新员工的竞争优势体现在哪里?
我想,老员工的竞争优势在于更能将技术优势发挥到极致,处理的量级更大,技术的研究深度无人替代。而这些优势的建立,离不开对技术有更深入的了解,知其然,知其所以然。
而大神,就是带你在熟悉的场景下见识下未曾见过的领域,让你深刻理解技术的本质。今天我就被见识了下。比如学习过 Python 的人对列表和元组的使用再熟悉不过,大家都知道除了元组中的元素不可修改,其他操作都很一样,除此之外还能说出他们的区别吗?比如说谁初始化更快,谁效率更高,谁更节省存储空间呢?
存储方式的差异
而大神通过几行代码就能教你如何判断,比如
l = [1, 2, 3]
l.__sizeof__()
64
tup = (1, 2, 3)
tup.__sizeof__()
48
你可以看到,对列表和元组,我们放置了相同的元素,但是元组的存储空间,却比列表要少 16 字节。这是为什么呢?
事实上,由于列表是动态的,所以它需要存储指针,来指向对应的元素(上述例子中,对于 int 型,8 字节)。另外,由于列表可变,所以需要额外存储已经分配的长度大小(8 字节),这样才可以实时追踪列表空间的使用情况,当空间不足时,及时分配额外空间。
l = []
l.__sizeof__() # 空列表的存储空间为 40 字节
40
l.append(1)
l.__sizeof__()
72 # 加入了元素 1 之后,列表为其分配了可以存储 4 个元素的空间 (72 - 40)/8 = 4
l.append(2)
l.__sizeof__()
72 # 由于之前分配了空间,所以加入元素 2,列表空间不变
l.append(3)
l.__sizeof__()
72 # 同上
l.append(4)
l.__sizeof__()
72 # 同上
l.append(5)
l.__sizeof__()
104 # 加入元素 5 之后,列表的空间不足,所以又额外分配了可以存储 4 个元素的空间
上面的例子,大概描述了列表空间分配的过程。我们可以看到,为了减小每次增加 / 删减操作时空间分配的开销,Python 每次分配空间时都会额外多分配一些,这样的机制(over-allocating)保证了其操作的高效性:增加 / 删除的时间复杂度均为 O(1)。
但是对于元组,情况就不同了。元组长度大小固定,元素不可变,所以存储空间固定。
看了前面的分析,你也许会觉得,这样的差异可以忽略不计。但是想象一下,如果列表和元组存储元素的个数是一亿,十亿甚至更大数量级时,你还能忽略这样的差异吗?
性能的差异
C:\Users\Administrator>python --version
Python 3.7.3
C:\Users\Administrator>python -m timeit -s "x=[1,2,3,4,5,6]"
10000000 loops, best of 5: 33.5 nsec per loop
C:\Users\Administrator>python -m timeit -s "x=(1,2,3,4,5,6)"
10000000 loops, best of 5: 30.5 nsec per loop
大神会通过这样的方式告诉你,元组初始化略微快一些,至于为什么,且听他道来:
Python 会在后台,对静态数据做一些资源缓存(resource caching)。通常来说,因为垃圾回收机制的存在,如果一些变量不被使用了,Python 就会回收它们所占用的内存,返还给操作系统,以便其他变量或其他应用使用。
但是对于一些静态变量,比如元组,如果它不被使用并且占用空间不大时,Python 会暂时缓存这部分内存。这样,下次我们再创建同样大小的元组时,Python 就可以不用再向操作系统发出请求,去寻找内存,而是可以直接分配之前缓存的内存空间,这样就能大大加快程序的运行速度。
使用场景
如果存储的数据和数量不变,比如你有一个函数,需要返回的是一个地点的经纬度,然后直接传给前端渲染,那么肯定选用元组更合适。
如果存储的数据或数量是可变的,比如社交平台上的一个日志功能,是统计一个用户在一周之内看了哪些用户的帖子,那么则用列表更合适。
思考题
以下两种方式初始化一个空列表,哪一种方式更高效? 原因是什么?
# 创建空列表
# option A
empty_list = list()
# option B
empty_list = []
我的感受
这些内容是我在自学 Python 时没有考虑到的,也不会从这些角度去思考的,学习一样东西,最好还是找这方面最厉害的老师,跟着老师去学,这样才进步最快。而 Facebook 资深工程师景霄就是这样的老师,让大神带我们从工程的角度学习 Python,成为 Python 高手。
福利不要错过: 扫下方二维码购买的,加我微信好友,我再给你返现 12 元,相当于 56 元购买,如果是新用户可以再此基础上再减 35 元。另外,接你加入我建的技术乱谈群,群内都是 Python 高手,大家可以相互交流,共同进步。
如果图片无法看到请点击链接
了解更多
列表和元组的内部实现都是数组的形式,列表因为可变,所以是一个 over-allocate 的数组,元组因为不可变,所以长度大小固定。具体可以参照源码:
列表 https://github.com/python/cpython/blob/master/Objects/listobject.c
元组 https://github.com/python/cpython/blob/master/Objects/tupleobject.c
最后的思考题:区别主要在于 list() 是一个函数调用,Python 的函数调用会创建栈,并且进行一系列参数检查的操作,比较耗时,反观 [] 是一个内置的 C 函数,可以直接被调用,因此效率高。内存分配,GC 等等知识会在第二章进阶里面专门讲到。
(完)
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于