Python小秘籍了解一下

Python小秘籍了解一下

本文参考资料为《python源码剖析》一书,想要深入理解python,该书可谓是经典之作,只是版本略老,书中使用的python还停留在2.5版本,不过笔者在阅读本书时参考的是python2.7.13和3.6.5的源码,有些地方与书中所讲有些许出入,笔者都加了备注。该书对python源码讲解的比较到位,主要的数据结构及原理都涉及到了,不过笔者觉得涉及源码的讲解不是很实用,了解一下还是可以的。不过有些部分讲的还是比较实用,对于日常的python编程还是很有帮助的,笔者只将这些部分结合自己的理解做了简单的总结。

1、名字引用查找顺序LEGB

LEGB是namespace(名字空间)即通常所指的作用域的名称的缩写

  • L:local作用域,函数内部
  • E: enclosing作用域,闭包
  • G:global作用域,module级别的作用域
  • B:builtin作用域,python内置的作用域,包含了常用的内部函数,比如:dir、open、range等

熟悉python的人应该很容易就脑补出各个作用域的范围,这里就不上圈圈圆圆圈圈的图了。不过还是贴一个无聊的例子,演示下同一模块中的名字引用:

当然通过一些关键字可以改变这种查找顺序,比如global、nonlocal,被这些关键字所修饰的变量名会跳过当前名字空间的查找,而直接跳到对应的名字空间去查找。

再来看一个不同模块间名字的引用

运行模块A时输出的为module B ,这就说明了LEGB规则不会跨module,同时也说明了一个重要的点:python的名字空间根据所写的代码文本格局就定义好了。结合例子来说就是:模块B中的show_owner所引用的owner在你保存代码的时候就已经被固定为是对模块B本身中定义的owner的引用了。

2、pyc

python在运行前会先对源码进行编译产生PyCodeObject对象,而这些对象则被保存于pyc文件中,再下一次运行相同代码时则直接从该pyc文件中读取PyCodeObject到内存中运行,避免了重复编译的过程。所以说我们通常所看到的pyc文件(字节码文件),其实只是一个存储载体。

3、整数缓存池

python虚拟机在初始化的时候会创建一个整数缓冲池[-5~257)。python中的每个整数都对应着一个PyIntObject对象,每次创建一个整数对象时都要向系统申请内存,用完后再释放,这是比较耗时的,而有了缓冲池则可以避免内存的申请与释放,提升性能。当创建一个整数变量时,如果该值符合缓存池的范围,则该变量直接引用缓冲池中对应的PyIntObject对象,如果该值不在缓冲池内,才会创建新的PyIntObject,通过例子来看下:

当值为256时则会直接引用缓冲池中的对象,所以此时a is b为True,因为他们引用的是同一个对象。当值为456时则会各自创建新的对象,此时a和b是不同的对象。

当然对于大整数即不在整数缓冲池中的整数而言,python也不会笨到每次都重新申请和销毁内存,而是采用了通用缓存池,也就是在创建了一个大整数对象后,该对象引用计数为0时会触发gc,但是该部分内存不会归还系统,而是被放到通用缓存池中,下次在创建大整数时,会先查看缓存池中是否有可用的内存,有则直接使用,避免了重新申请内存的过程。

注: 通用缓存池有内存泄露的风险,试想如果创建了大量的大整数,都在被引用,当这些大整数引用计数为0时,所占内存都被放到缓冲池中了,而没有归还系统,很明显的内存泄露,不过这只是python25和27中存在的情况。笔者在python3.6.5的源码中没有发现关于通用缓存池相关的定义,可能是去掉了,亦或是改为其他方式挪到其他地方了,但笔者相信新版的python中不会存在这样的问题,毕竟随着版本的升级,遗留的问题都会被以不同的方式修复

4、dict

python中的dict是以散列表数据结构实现的,当要存储一个entry(key-value对)时,首先会通过散列函数,以key为参数计算得到一个散列值(一个整数),即f(key)->n,然后将value存入位置为n的内存中。如果字典中存储大量的entry时,f(key)可能会得到相同的散列值,这种情况称为散列冲突,python对于散列冲突采取了开放定址法,即当出现冲突时会使用一个探测函数重新计算出一个新的散列值,如果仍存在冲突则继续使用探测函数计算,直到没有冲突。还有一种处理散列冲突的方法为开链法,即在冲突的散列值位置处创建一个链表,将所有冲突的key对应的value放入该链表中,因为链表的查询效率较低,所以该种方法在entry量很大时效率偏低。以上是对dict实现的一个大致概述,其中有几点需要提一下:

1、dict的初始容量为8,也就是存入的entry数量小于8时不需要申请内存

2、当dict已使用的容量超过总容量的2/3(该值被称为装载因子)时则会自动扩充容量,扩充的规则根据python规则有所不同:

  • python25和27-当已用容量used < 50k ,则总容量扩充为used*4,否则总容量为used*2,这么做也是考虑到系统内存不足的情况,扩充的倍数相应的减少
  • python3.2是used*4
  • python3.3是used*2
  • python3.6是扩充为used*2 + capacity/2 (capacity为当前总容量)

注:以上规则是笔者参考了python2.7和3.6的源码得出,python2.7与书中python2.5的扩充规则保持一致,python3.2和3.3的扩充规则在python3.6的源码注释中标注了。

5、系统路径的初始化

解释器在初始化时会加载site.py,其作用有两个:

  • 将site-packages路径加入sys.path;
  • 将site-packages目录下的.pth文件中配置的路径添加到sys.path

所以配置系统路径的方式,除了直接在代码中操作sys.path和配置系统变量PYTHONPATH外,还可以直接将模块代码放到site-packages下或者将模块路径配置到.pth文件中,至于怎么选择,还是那句话,依实际情况而定。

6、模块导入缓冲池

sys.moudles作为导入模块的缓冲池,解释器初始化时会加载部分常用模块到sys.moudles中,之后导入新的模块会将该模块添加到缓冲池中,再次导入则直接从缓冲池中加载。其实python中很多地方都用到了缓冲池技术,比如上边提到的整数缓冲池,包括dict的创建也使用了缓冲池技术。

7、内存缓冲池

内存缓冲池简称内存池,其中管理着很多小的内存块,当程序申请的内存小于某个阈值threshold时,会直接从内存池中获取,反之则最底层的内存管理接口向操作系统申请内存。

  • 书中所采用的python2.5的threshold为256字节
  • python2.7和3.6的threshold为512字节

8、GIL

GIL(Gloabl Interpreter Lock)全局解释器锁,一把至高无上的全局锁,表示着任何时刻只能有一个线程会获得该锁即获得解释器执行权限,那什么时候会释放该锁呢?python维护了一个阈值,获得GIL的线程一旦符合该阈值条件,就会释放锁,不同版本维护的阈值条件不同:

  • python2.7使用sys.get/setcheckinterval(interval)来获取设置阈值interval,该值为整型,表示每执行N条字节码指令就释放GIL,默认值为100,这与书中用的python2.5所说的保持一致。
  • python3.6使用sys.get/setswitchinterval(interval)来获取和设置阈值interval,该值为浮点型,表示执行N秒后释放GIL。

释放了GIL后,至于哪个线程会获得锁,这是操作系统调度的,python解释器无法控制。支持线程的语言在线程调度方面都是由OS支配的,比如java,虽然可以设置线程的优先级,但是实际的调度还是由OS来决定,毕竟线程还是OS级别的,还是得它亲爹去管理。

9、GC机制

python中有两种gc机制,引用计数机制和深度gc机制(书中没有命名,为了方便记忆,笔者自定义了一个):
1、引用计数是自动触发,当对象的引用计数为0时则立马会回收该对象内存,
2、深度gc是为循环引用问题而提供的。先简单说一下何为循环引用:循环引用是两个container对象在各自内部都包含对方的一个引用,形成了一个闭环。container对象是内部可持有其他对象引用的对象,比如list, dict,class,instance等,循环引用只能发生在container对象之间。深度gc机制采用了标记-清除算法和分代算法来最大限度的消除循环引用所带来的内存泄露问题,关于这两种算法熟悉JVM的同学应该不陌生,这里就不表了。

10、python之禅

没事儿就拉出来瞅一眼,顿觉神清气爽,茅塞顿开,写代码更有劲了

说点什么

avatar
  订阅  
提醒