上篇文章《Python是否支持复制字符串呢?》刚发出一会,@发条橙 同窗就在后台留言,指出了一处错误。我一惊,立刻去验证,居然真的错了,并且在彻底没意料到的地方!我开始觉得只是疏漏,一细想,发现不简单,遇到了百思不得其解的问题了。因此,这篇文章还得再聊聊字符串。python
照例先总结下本文内容:(1)join() 方法除了在拼接字符串时速度较快,它仍是目前看来最通用有效的复制字符串的方法 (2)Intern 机制(字符串滞留)并不是万能的,本文探索一下它的软肋有哪些微信
我先把那个问题化简一下吧:ide
ss0 = 'hi' ss1 = 'h' + 'i' ss2 = ''.join(ss0) print(ss0 == ss1 == ss2) >>> True print(id(ss0) == id(ss1)) >>> True print(id(ss0) == id(ss2)) >>> False
上面代码中,奇怪的地方就在于 ss2 居然是一个独立的对象!按照最初想固然的认知,我认定它会被 Intern 机制处理掉,因此是不会占用独立内存的。上篇文章快写完的时候,我忽然想到 join 方法,因此没作验证就临时加进去,致使了意外的发生。性能
按照以前在“特权种族”那篇文章的总结,我对字符串 Intern 机制有这样的认识:学习
Python中,字符串使用Intern机制实现内存地址共用,长度不超过20,且仅包括下划线、数字、字母的字符串才会被intern;涉及字符串拼接时,编译期优化结果会与运行期计算结果不一样。
为何 join 方法拼接字符串时,能够不受 Intern 机制做用呢?优化
回看那篇文章,发现可能存在编译期与运行期的差异!网站
# 编译对字符串拼接的影响 s1 = "hell" s2 = "hello" "hell" + "o" is s2 >>>True s1 + "o" is s2 >>>False # "hell" + "o"在编译时变成了"hello", # 而s1+"o"由于s1是一个变量,在运行时才拼接,因此没有被intern
实验一下,看看:ui
# 代码加上 ss3 = ''.join('hi') print(id(ss0) == id(ss3)) >>> False
ss3 仍然是独立对象,难道这种写法仍是在运行期时拼接?那怎么判断某种写法在编译期仍是在运行期起做用呢?继续实验:spa
s0 = "Python猫" import copy s1 = copy.copy(s0) s2 = copy.copy("Python猫") print(id(s0) == id(s1)) >>> True print(id(s0) == id(s2)) >>> False
看来,不能经过是否显性传值来判断。3d
那就只能从 join 方法的实现原理入手查看了。经某交流群的小伙伴提醒,能够去 Python Tutor 网站,看看可视化执行过程。可是,很遗憾,也没看出什么底层机制。
我找了分析 CPython 源码的资料(含上期荐书栏目的《Python源码剖析》)来学习,可是,这些资料只比较 join() 方法与 + 号拼接法在原理与使用内存上的差别,并没说起为什么 Intern 机制对前者会失效,而对后者倒是生效的。
现象已经产生,我只能暂时解释说,join 方法会不受 Intern 机制控制,它有独享内存的“特权”。
那就是说,其实有复制字符串的方法!上篇《Python是否支持复制字符串呢?》因为没有发现这点,最后得出了错误的结论!
因为这个特例,我要修改上篇文章的结论了:Python 自己并不限制字符串的复制操做,CPython 解释器出于优化性能的考虑,加入了一些小把戏,试图使字符串对象在内存中只有一份,尽管如此,仍存在有效复制字符串的方法,那就是 join() 方法。
join() 方法的神奇用处使我不得不改变对 Intern 机制的认识,本小节就带你们从新学习一下 Intern 机制吧。
所谓 Intern 机制,即字符串滞留(string interning),它经过维护一个字符串常量池(string intern pool),从而试图只保存惟一的字符串对象,达到既高效又节省内存地处理字符串的目的。在建立一个新的字符串对象后,Python 先比较常量池中是否有相同的对象(interned),有的话则将指针指向已有对象,并减小新对象的指针,新对象因为没有引用计数,就会被垃圾回收机制回收掉,释放出内存。
Intern 机制不会减小新对象的建立与销毁,但最终会节省出内存。这种机制还有另外一个好处,即被 Interned 的相同字符串做比较时,几乎不花时间。实验数据以下(资料来源:http://t.cn/ELu9n7R):
Intern 机制的大体原理很好理解,然而影响结果的还有 CPython 解释器的其它编译及运行机制,字符串对象受到这些机制的共同影响。实际上,只有那些“看起来像” Python 标识符的字符串才会被处理。源代码StringObject.h
的注释中写道:
/ … … This is generally restricted to strings that “looklike” Python identifiers, although the intern() builtin can be used to force interning of any string … … /
这些机制的相互做用,不经意间带来了很多混乱的现象:
# 长度超过20,不被intern VS 被intern 'a' * 21 is 'aaaaaaaaaaaaaaaaaaaaa' >>> False 'aaaaaaaaaaaaaaaaaaaaa' is 'aaaaaaaaaaaaaaaaaaaaa' >>> True # 长度不超过20,不被intern VS 被intern s = 'a' s * 5 is 'aaaaa' >>> False 'a' * 5 is 'aaaaa' >>> True # join方法,不被intern VS 被intern ''.join('hi') is 'hi' >>> False ''.join('h') is 'h' >>> True # 特殊符号,不被intern VS 被"intern" 'python!' is 'python!' >>> False a, b = 'python!', 'python!' a is b >>> True
这些现象固然都能被合理解释,然而因为不一样机制的混合做用,就很容易形成误会。好比第一个例子,不少介绍 Intern 机制的文章在比较出 'a' * 21
的id有变化后,就认为 Intern 机制只对长度不超过20的字符串生效,但是,当看到长度超过20的字符串的id还相等时,这个结论就变错误了。当加入常量合并(Constant folding)
的机制后,长度不超过20的字符串会被合并的现象才获得解释。但是,在 CPython 的源码中,只有长度不超过1字节的字符串才会被 intern ,为什么长度超标的状况也出现了呢? 再加入 CPython 的编译优化机制,才能解释。
因此,看似被 intern 的两个字符串,实际可能不是 Intern 机制的结果,而是其它机制的结果。一样地,看似不能被 intern 的两个字符串,实际可能被其它机制以相似方式处理了。
如此种种,便提升了理解 Intern 机制的难度。
就我在上篇文章中所关心的“复制字符串”话题而言,只有当 Intern 机制与其它这些机制通通失效时,才能作到复制字符串。目前看来,join 方法最具通用性。
总而言之,由于从新学习 join 方法的神奇用处与 Intern 机制的例外状况,我得以修正上篇文章的错误。在此过程当中,我获得了新的知识,以及思考学习的乐趣。
《超人》电影中有一句著名的台词,在今年上映的《头号玩家》中也出现了:
有的人从《战争与和平》里看到的只是一个普通的冒险故事,有的人则能经过阅读口香糖包装纸上的成分表来解开宇宙的奥秘。
我读到的是一种敏锐思辨的思想、孜孜求索的态度和以小窥大的方法。做为一个低天赋的人,受此鼓舞,我会继续追问那些看似没意义的问题(“如何删除字符串”、“如何复制字符串”...),一点一点地学习 Python ,以个人方式理解它。同时,但愿能给个人读者们带来一些收获。
PS.很多人在期待 “Python猫” 系列,别急哈,让那只猫再睡几天,等它醒来,我替你们催它!
字符串系列文章:
Python猫系列:
-----------------
本文原创并首发于微信公众号【Python猫】,后台回复“爱学习”,免费得到20+本精选电子书。