TA的每日心情 | 无聊 3 小时前 |
---|
签到天数: 1052 天 连续签到: 2 天 [LV.10]测试总司令
|
本文标题的问题分为两部分:(1)Python 中是否支持复制字符串?(2)如果不支持,为什么不支持?
请读者花几分钟想一下,想清楚后,把你的答案记住,然后再往下看。
让我们做一个约定(自愿遵守):如果看到最后,你推翻了现在的答案,建立了新的认知,这说明我写的内容有用,那请你任意赞赏,或者将本文分享给其他使用 Python 的小伙伴。
1. 什么是复制字符串?
首先,必须要大家对“复制”这个概念达成共识。复制,也叫拷贝,英文单词是 copy,具体意思是“将某事物通过某种方式制作成相同的一份或多份的行为”(释义来自维基百科)。复制的结果是,出现了多份极其相似但却相互独立的事物(副本),举例来说,你有一份文档 X,然后复制一份并重新命名为 Y,这两者是相互独立的,若你删除其中一个,另一个不会一起被删除。
这个词用在 Python 里,我们想表达的是同样的意思,即复制行为会产生新的独立对象,它与原始对象极其相似,但两者的生命周期没有直接的关联关系。下面先用列表来举例:
- list1 = [1,2]
- id(list1)
- >>> 1981119454856
- list2 = list1.copy()
- print(list1 == list2)
- >>> True
- id(list2)
- >>> 1981116983752
复制代码 上例中,列表 list2 是 list1 的副本,两者字面量相等,但是内存地址(即 id )不相等,是两个相互独立的对象。如果字符串能够做到同样的效果,那我们就说,字符串可以被复制,否则,我们说字符串不可以被复制。
2. 怎样能复制字符串?
有了上面的概念和示例,请先思考,你会用什么方式复制字符串呢?(暂停,思考3分钟)
好了,先看看下面的几种方法:
- s0 = "Python编程学习圈"
- s1 = s0
- s2 = str(s0)
- s3 = s0[:]
- s4 = s0 + ''
- s5 = '%s' % s0
- s6 = s0 * 1
- s7 = "".join(s0)
- import copy
- s8 = copy.copy(s0)
复制代码 你想到的复制方式是否在以上8种方式里呢?那么,如果把 s0 至 s8 的 id 打印出来,有哪些会跟 s0 不同呢?
答案是,它们的内存地址 id 完全相同,也就是说,一顿操作猛如虎,结果却始终只有一份字符串,根本没有复制出新的字符串!
会心一笑,这不就是因为字符串的 Intern 机制嘛,短字符串在内存中只会存在一份。
但请别开心得太早,你可以把 s0 改成一个超长的字符串,例如:
- s0 = "Python编程学习圈,正在学习Python,它的微信公众号也叫Python编程学习圈,欢迎你关注哦"
复制代码 然后,再重复上面的操作。最终,你会发现,s0 到 s8 的 id 还是完全相同。
是不是吃惊了呢?新的 s0 明明已经超过 Intern 机制的长度了,为什么不会产生新的字符串呢?
首先,请你相信,超出 Intern 机制的字符串可以存在多份,即你可以创建出值完全相同的多个字符串对象,因为字符串对象在内存中并不一定是唯一的:
- s9 = "Python编程学习圈,正在学习Python,它的微信公众号也叫Python编程学习圈,欢迎你关注哦"
- print(id(s0) == id(s9))
- >>> False
复制代码 上例表明,你可以创建出多个相同的字符串对象,但是这种方法与前面列举的8种不同,因为它是独立于 s0 的操作,并不是一种复制操作。从理论上讲,Python 完全可以提供一个方法,达到复制出新的副本的结果。现在的问题恰恰就是:为什么允许存在多个相等的字符串对象,但是却无法通过复制的方式来创建呢?
为什么不允许复制字符串?
我发现,不仅字符串不允许复制,元组也如此,事实上,还有 int 、float 也不支持复制。它们都是不可变对象,为什么不可变对象就不支持复制操作呢?
在查资料的时候,我发现网上很多文章对于“不可变对象”的认识存在误区,这些人不知道 Intern 机制的存在,误以为字符串对象在内存只能有唯一一个,进而误以为不可变对象就是在内存中只有一份的对象。所以,这些文章很容易推断出错误的结论:因为字符串是不可变对象,所以字符串不支持复制。
事实上,不可变对象跟复制操作之间,并没有必然的强相关的关系。肯定是出于别的原因,设计者才给不可变对象加上这种限制,这个原因是什么呢?
在知乎上,有敏锐的同学提出了我的疑问“Python中如何复制一个值或字符串?”,可惜只有4个回答,而且都没答到点上。Stackoverflow上恰好也有一个问题“How can I copy a Python string?”,同样没多少人注意到,只有5个回答,好在最高票答案提到了一个点,即这样可以加快字典的查找速度。
然而,他说的这个点并不靠谱。字典要求键值是可哈希对象,可是计算字符串的哈希值是根据字面值计算,所以对多个相等的字符串对象,其哈希值其实是一样的,对计算和查找根本无影响。
- w1 = "Python编程学习圈,正在学习Python,它的微信公众号也叫Python编程学习圈,欢迎你关注哦"
- w2 = "Python编程学习圈,正在学习Python,它的微信公众号也叫Python编程学习圈,欢迎你关注哦"
- print(w1 == w2)
- >>> True
- print(id(w1) == id(w2))
- >>> False
- print(hash(w1) == hash(w2))
- >>> True
复制代码 继续查资料,终于在《流畅的Python》找到了明确的解释:
这些细节是 CPython 核心开发者走的捷径和做的优化措施,对这门语言的用户而言无需了解,而且那些细节对其他 Python 实现可能没用,CPython 未来的版本可能也不会用。
这本《流畅的Python》是进阶首选书目之一,我曾读过部分章节,没想到在一个不起眼的小节里,作者 “惊讶地发现” 元组的不可复制性,在此之前,他还自以为“对元组无所不知”,哈哈哈。
虽然,我早猜测到原因是节省内存和提高速度,但看到这个明确的解释,知道这只是CPython 解释器的“善意的谎言”,而且在未来版本可能不会用,我感到特别意外。
它证实了我的猜测,同时,也提供了超预期的信息:其它 Python 解释器可能支持复制不可变对象,目前 CPython 算是一种妥协,在未来可能会恢复不可变对象的复制操作呢!
回到文章开头的两个问题,我们得到的答案是:Python 本身并不限制字符串的复制操作,只是当前版本的 CPython 做了优化,才导致出现这种“善意的谎言”,它这么做的原因为了对 Intern 机制做补充,设法使全部字符串对象在内存都只有一份,以达到节省内存的效果。
CPython 是用 C 语言实现的 Python 解释器,是官方的、使用最广泛的解释器。除了它,还有用 [url=]Java[/url] 实现的 Jython 解释器、用 .NET 实现的 IronPython 解释器、用 Python 实现的 PyPy 解释器,等等。其它解释器都是怎么应对字符串的复制操作的呢?唉,学无止境,本人才疏学浅没有涉猎,还是先搁置疑问吧。
这里,我就想提一个题外话,Python 最最最广为人诟病的就是 GIL(全局解释器锁),这导致它不支持真正意义的多线程,成为很多人指责 Python 慢的元凶。但是,这个问题是 CPython 解释器带来的,而像 Jython 解释器就不存在这个问题。
|
|