×
嵌入式 > 详情

什么是复制字符串?Python如何复制字符串

发布时间:2021-05-11 发布时间:
|

连续几篇文章都在写Python字符串,这出乎我的意料了。但是,有的问题,不写不行,特别是那种灵机一动想到的问题,最后你发现,很多人根本不懂却又误以为自己懂了。那就继续刨根问底,探究个明白吧。

在上一篇文章《你真的知道Python的字符串怎么用吗?》里,我突发奇想,将字符串跟列表做了比较,然后发现字符串竟然没有复制的方法。当时没有细想,只说要搁置疑问。过后,有好学的小伙伴在后台留言,与我交流这个问题,给了我一些启发。为了彻底弄懂它,我继续查了不少资料,今天,就跟大家分享一下我发现的东西吧。

本文标题的问题分为两部分:(1)Python中是否支持复制字符串?(2)如果不支持,为什么不支持?

请读者花几分钟想一下,想清楚后,把你的答案记住,然后再往下看。

让我们做一个约定(自愿遵守):如果看到最后,你推翻了现在的答案,建立了新的认知,这说明我写的内容有用,那请你任意赞赏,或者将本文分享给其他使用Python的小伙伴。

1.什么是复制字符串?

首先,必须要大家对“复制”这个概念达成共识。复制,也叫拷贝,英文单词是copy,具体意思是“将某事物通过某种方式制作成相同的一份或多份的行为”(释义来自维基百科)。复制的结果是,出现了多份极其相似但却相互独立的事物(副本),举例来说,你有一份文档X,然后复制一份并重新命名为Y,这两者是相互独立的,若你删除其中一个,另一个不会一起被删除。

这个词用在Python里,我们想表达的是同样的意思,即复制行为会产生新的独立对象,它与原始对象极其相似,但两者的生命周期没有直接的关联关系。下面先用列表来举例:

list1=[1,2]id(list1)>>>1981119454856list2=list1.copy()print(list1==list2)>>>Trueid(list2)>>>1981116983752

上例中,列表list2是list1的副本,两者字面量相等,但是内存地址(即id)不相等,是两个相互独立的对象。如果字符串能够做到同样的效果,那我们就说,字符串可以被复制,否则,我们说字符串不可以被复制。

2.怎样能复制字符串?

有了上面的概念和示例,请先思考,你会用什么方式复制字符串呢?(暂停,思考3分钟)

好了,先看看下面的几种方法:

s0="Python猫"s1=s0s2=str(s0)s3=s0[:]s4=s0+''s5='%s'%s0s6=s0*1s7="".join(s0)importcopys8=copy.copy(s0)

你想到的复制方式是否在以上8种方式里呢?那么,如果把s0至s8的id打印出来,有哪些会跟s0不同呢?

答案是,它们的内存地址id完全相同,也就是说,一顿操作猛如虎,结果却始终只有一份字符串,根本没有复制出新的字符串!

Python猫的老读者看到这,会心一笑,这不就是因为字符串的Intern机制嘛,短字符串在内存中只会存在一份,在《Python中的“特权种族”是什么?》这篇文章里提到过的。

但请别开心得太早,你可以把s0改成一个超长的字符串,例如:

s0="Python猫是来自喵星的客人,它喜欢地球和人类,正在学习Python,而且想借助Python变成人,它的微信公众号也叫Python猫,欢迎你关注哦,喵喵喵喵~~~"

然后,再重复上面的操作。最终,你会发现,s0到s8的id还是完全相同。

是不是吃惊了呢?新的s0明明已经超过Intern机制的长度了,为什么不会产生新的字符串呢?

首先,请你相信,超出Intern机制的字符串可以存在多份,即你可以创建出值完全相同的多个字符串对象,因为字符串对象在内存中并不一定是唯一的:

s9="Python猫是来自喵星的客人,它喜欢地球和人类,正在学习Python,而且想借助Python变成人,它的微信公众号也叫Python猫,欢迎你关注哦,喵喵喵喵~~~"print(id(s0)==id(s9))>>>False

上例表明,你可以创建出多个相同的字符串对象,但是这种方法与前面列举的8种不同,因为它是独立于s0的操作,并不是一种复制操作。从理论上讲,Python完全可以提供一个方法,达到复制出新的副本的结果。现在的问题恰恰就是:为什么允许存在多个相等的字符串对象,但是却无法通过复制的方式来创建呢?

3.为什么不允许复制字符串?

我发现,不仅字符串不允许复制,元组也如此,事实上,还有int、float也不支持复制。它们都是不可变对象,为什么不可变对象就不支持复制操作呢?

在查资料的时候,我发现网上很多文章对于“不可变对象”的认识存在误区,这些人不知道Intern机制的存在,误以为字符串对象在内存只能有唯一一个,进而误以为不可变对象就是在内存中只有一份的对象。所以,这些文章很容易推断出错误的结论:因为字符串是不可变对象,所以字符串不支持复制。

事实上,不可变对象跟复制操作之间,并没有必然的强相关的关系。肯定是出于别的原因,设计者才给不可变对象加上这种限制,这个原因是什么呢?

在知乎上,有敏锐的同学提出了我的疑问“Python中如何复制一个值或字符串?”,可惜只有4个回答,而且都没答到点上。Stackoverflow上恰好也有一个问题“HowcanIcopyaPythonstring?”,同样没多少人注意到,只有5个回答,好在最高票答案提到了一个点,即这样可以加快字典的查找速度。

然而,他说的这个点并不靠谱。字典要求键值是可哈希对象,可是计算字符串的哈希值是根据字面值计算,所以对多个相等的字符串对象,其哈希值其实是一样的,对计算和查找根本无影响。

w1="Python猫是来自喵星的客人,它喜欢地球和人类,正在学习Python,而且想借助Python变成人,它的微信公众号也叫Python猫,欢迎你关注哦,喵喵喵喵~~~"w2="Python猫是来自喵星的客人,它喜欢地球和人类,正在学习Python,而且想借助Python变成人,它的微信公众号也叫Python猫,欢迎你关注哦,喵喵喵喵~~~"print(w1==w2)>>>Trueprint(id(w1)==id(w2))>>>Falseprint(hash(w1)==hash(w2))>>>True

继续查资料,终于在《流畅的Python》找到了明确的解释:

这些细节是CPython核心开发者走的捷径和做的优化措施,对这门语言的用户而言无需了解,而且那些细节对其他Python实现可能没用,CPython未来的版本可能也不会用。

这本《流畅的Python》是进阶首选书目之一,我曾读过部分章节,没想到在一个不起眼的小节里,作者“惊讶地发现”元组的不可复制性,在此之前,他还自以为“对元组无所不知”,哈哈哈。

虽然,我早猜测到原因是节省内存和提高速度,但看到这个明确的解释,知道这只是CPython解释器的“善意的谎言”,而且在未来版本可能不会用,我感到特别意外。

它证实了我的猜测,同时,也提供了超预期的信息:其它Python解释器可能支持复制不可变对象,目前CPython算是一种妥协,在未来可能会恢复不可变对象的复制操作呢!

回到文章开头的两个问题,我们得到的答案是:Python本身并不限制字符串的复制操作,只是当前版本的CPython做了优化,才导致出现这种“善意的谎言”,它这么做的原因为了对Intern机制做补充,设法使全部字符串对象在内存都只有一份,以达到节省内存的效果。

CPython是用C语言实现的Python解释器,是官方的、使用最广泛的解释器。除了它,还有用Java实现的Jython解释器、用.NET实现的IronPython解释器、用Python实现的PyPy解释器,等等。其它解释器都是怎么应对字符串的复制操作的呢?唉,学无止境,本人才疏学浅没有涉猎,还是先搁置疑问吧。

这里,我就想提一个题外话,Python最最最广为人诟病的就是GIL(全局解释器锁),这导致它不支持真正意义的多线程,成为很多人指责Python慢的元凶。但是,这个问题是CPython解释器带来的,而像Jython解释器就不存在这个问题。

好了,就此打住吧。你是否还记得在文章开头时想到的答案呢?是否改变了最初的想法呢?欢迎关注公众号Python猫,来跟我交流,一起来学习Python,做个合格的Pythonista。

 



『本文转载自网络,版权归原作者所有,如有侵权请联系删除』

热门文章 更多
轻量级嵌入式GUI高级功能实现