帶你從內(nèi)存的角度看Python中的變量
1、前言
由于筆者并未系統(tǒng)地學(xué)習(xí)過Python,對(duì)Python某些底層的實(shí)現(xiàn)細(xì)節(jié)一概不清楚,以至于在實(shí)際使用的時(shí)候會(huì)寫出一些奇奇怪怪的Bug(沒錯(cuò),別人寫代碼,我寫B(tài)ug),比如對(duì)象的某些屬性莫名奇妙地改變。究其原因,是對(duì)Python中的變量機(jī)制存在一些誤解,畢竟以前一直是用C語言居多。無奈,只能深入學(xué)習(xí)這一部分的知識(shí),并總結(jié)成此文。
閱讀本文,你可以:
- 了解Python中變量的“儲(chǔ)存”機(jī)制。
- 了解Python中賦值、淺拷貝于深拷貝的區(qū)別和使用場(chǎng)景。
- 了解Python中的函數(shù)傳參形式。
當(dāng)然,你需要一點(diǎn)基礎(chǔ)的編程和面向?qū)ο蟮闹R(shí)才能看懂本文。
2、引用式變量
相信學(xué)過Python的小伙伴都聽過這樣一句話:Python中一切皆是對(duì)象。這意味著,哪怕是Python中的基本數(shù)據(jù)類型,其本質(zhì)上也是對(duì)象,例如對(duì)于一個(gè)int類型的變量a,你可以調(diào)用int類對(duì)象的方法來求a的絕對(duì)值:
>>> a = -1 >>> a.__abs__() 1
在這個(gè)例子中,可以說:a是int類的一個(gè)實(shí)例對(duì)象,其值是-1。當(dāng)然,這句話其實(shí)說的不對(duì),因?yàn)閍并不是一個(gè)對(duì)象,而是對(duì)象的引用。這聽起來很奇怪,但事實(shí)就是如此。Python中的變量都是引用式變量,他并不像C/C++中的變量,儲(chǔ)存著具體的數(shù)據(jù)類型或?qū)ο?,他像是C++中的引用。通俗的講,Python中的變量相當(dāng)于對(duì)象的別名,如果你有C語言的基礎(chǔ),可以把它理解為C語言中的指針,通過它你可以在內(nèi)存中找到對(duì)象。話不多說,先看圖:
左邊的圖表示的就是C語言中的變量,變量相當(dāng)于一個(gè)“盒子”,“盒子”里裝著值,右邊表示的就是Python中的引用式變量,a和b都是列表對(duì)象[1, 2, 3]的別名,像是貼在[1, 2, 3]上的”標(biāo)簽“,順著這些”標(biāo)簽“,解釋器可以在內(nèi)存中找到他們對(duì)應(yīng)的對(duì)象。你也許會(huì)問,這有啥區(qū)別,不都是變量嗎。還是先看代碼:
a = [1, 2, 3] b = a a[2] = 9 print(a) print(b)
----運(yùn)行結(jié)果----
[1, 2, 9]
[1, 2, 9]
意想不到的事情發(fā)生了,明明代碼只改變了a的值,為什么b也跟著變了呢?這是因?yàn)椋琣、b都是列表的引用,并不是實(shí)際的列表,上述代碼通過a這個(gè)”標(biāo)簽“改變了內(nèi)存中列表[1, 2 ,3]的值,于是乎,你順著b”標(biāo)簽“找到的列表,當(dāng)然是改變了的。再看代碼:
a = [1, 2, 3] b = a a = [1, 2, 9] print(a) print(b)
----運(yùn)行結(jié)果----
[1, 2, 9]
[1, 2, 3]
在這個(gè)例程中,我們把[1, 2, 9]賦值給了a,然后再輸出a和b,此時(shí)a已經(jīng)發(fā)生變化,而b沒有改變,a從列表[1, 2, 3]的引用變成了列表[1, 2, 9]的引用,列表[1, 2, 3]在內(nèi)存中并未發(fā)生任何改變,這就是b輸出的值不發(fā)生變化的原因。到這里,你應(yīng)該可以理解上面說的:a是int類的一個(gè)實(shí)例對(duì)象,其值是-1為什么是錯(cuò)的了。這樣的賦值語句在Python中的應(yīng)該這樣理解:創(chuàng)建一個(gè)int類對(duì)象-1,讓a作為-1的引用。當(dāng)然,右邊的值是常量或是可變對(duì)象,解釋器都會(huì)做出不同的反應(yīng),這將在下文進(jìn)一步講解。總之,啰啰嗦嗦說了這么多,就是希望大家都能搞明白這個(gè)問題,核心就是一句話:Python中的變量都是引用式變量,變量存儲(chǔ)的不是值,而是引用。
3、賦值、淺拷貝與深拷貝
看完上一節(jié),肯定有人會(huì)問,如果Python中的賦值都是引用,那我想創(chuàng)建一個(gè)變量的副本做備份怎么辦?這在C語言中簡(jiǎn)單的一句b=a就可以實(shí)現(xiàn)的需求在Python中如何實(shí)現(xiàn)?Python中提供了三種復(fù)制的方式,即:
- 賦值:創(chuàng)建對(duì)象的引用。
- 淺拷貝:拷貝對(duì)象,但不拷貝對(duì)象內(nèi)部的子對(duì)象。
- 深拷貝:拷貝對(duì)象,并且拷貝對(duì)象內(nèi)部的子對(duì)象。
一如既往地先看代碼,畢竟代碼最能說明問題:
import copy a = [1, 2, [3, 3 , 3], [4, 4]] b = a # 賦值 c = a.copy() # 淺拷貝,調(diào)用對(duì)象的copy()方法 d = copy.deepcopy(a) # 深拷貝,需要引入copy模塊,使用deepcopy()方法 a[1] = -2 # 改變1 a[2] = [-3, -3, -3] # 改變2 a[3][0] = -4 # 改變3 print(a) print(b) print(c) print(d)
----運(yùn)行結(jié)果----
[1, -2, [-3, -3, -3], [-4, 4]]
[1, -2, [-3, -3, -3], [-4, 4]]
[1, 2, [3, 3, 3], [-4, 4]]
[1, 2, [3, 3, 3], [4, 4]]
為了更方便闡述,這里我先給出這個(gè)例程中對(duì)象在內(nèi)存中的變化情況,當(dāng)然我更建議你自己去這個(gè)網(wǎng)站逐步可視化地運(yùn)行上面的代碼,甚至是本文中的所有代碼,這能加深你的理解。
在這段代碼中,首先創(chuàng)建了一個(gè)列表對(duì)象,這個(gè)列表的第3、4個(gè)元素也是列表對(duì)象,a是這個(gè)列表的引用,把a(bǔ)賦值給b,此時(shí)b也是同一個(gè)對(duì)象的引用,在內(nèi)存中,它們指向同一個(gè)對(duì)象,因此可以看到無論怎么通過a改變這個(gè)對(duì)象,a和b都是相同的。c則是對(duì)a的淺拷貝,解釋器新開辟了一塊內(nèi)存,存儲(chǔ)了原列表的一個(gè)副本,但是由于是淺拷貝,對(duì)象內(nèi)部的子對(duì)象沒有被拷貝。因此,這個(gè)副本列表的后面兩個(gè)元素依舊和原列表一樣,是列表[3, 3 , 3]和[4, 4]的引用,在內(nèi)存中指向同樣的對(duì)象。代碼中的改變2讓原列表的第三個(gè)元素變成了另一個(gè)列表[-3, -3 , -3]的引用,但是這個(gè)副本列表的第三個(gè)元素還是[3, 3 , 3]的引用。改變3則修給了原列表第四個(gè)元素指向的列表中的一個(gè)元素,因此打印c你會(huì)發(fā)現(xiàn)它指向的列表對(duì)應(yīng)位置的元素也改變了。而對(duì)于d,d是a的深拷貝,解釋器新開辟了一塊內(nèi)存,完全復(fù)制了原列表對(duì)象(包括子列表對(duì)象)放在這塊內(nèi)存中。因此,d指向的對(duì)象和a指向的對(duì)象沒有任何關(guān)系,無論怎么改變a指向的那個(gè)列表,都不會(huì)影響d指向的列表。
看到這里,你應(yīng)該知道如何實(shí)現(xiàn)本節(jié)開頭的需求了。
4、is的用法和id()函數(shù)
在Python中,每個(gè)對(duì)象都有各自的編號(hào)、類型和值,一個(gè)對(duì)象被創(chuàng)建以后,它的編號(hào)就不會(huì)改變,可以理解為對(duì)象在內(nèi)存中的地址。id()函數(shù)可以獲取對(duì)象的編號(hào),在CPython解釋器中,這個(gè)編號(hào)就是對(duì)象在內(nèi)存中的地址。is是一個(gè)雙目運(yùn)算符,運(yùn)算結(jié)果是布爾變量,用來比較兩個(gè)對(duì)象的編號(hào)是否相同,準(zhǔn)確的說,可以用于比較兩個(gè)變量是否是同一個(gè)對(duì)象的引用。
a = [1, 2, 3] b = a # 賦值 c = a.copy() # 淺拷貝 print(id(a)) print(id(b)) print(id(c)) print(a is b) print(a is c)
----運(yùn)行結(jié)果----
2667871075272
2667871075272
2667871075208
True
False
顯然,a、b是同一個(gè)對(duì)象的引用,而c是淺拷貝的副本,因此a和c引用的不是同一個(gè)對(duì)象,即使這兩個(gè)對(duì)象的值相等。不知你是否還記得,第1節(jié)中還提到在賦值語句中,右邊是可變對(duì)象與不可變對(duì)象,解釋器會(huì)由不同的操作,比如下面的代碼:
a = 5 b = 5 print(a is b) c = [1, 2, 3] d = [1, 2, 3] print(c is d)
----運(yùn)行結(jié)果----
True
False
對(duì)a、b分別賦值為5,但是它們卻是同一個(gè)對(duì)象的引用,這是因?yàn)椋?是一個(gè)常量,對(duì)應(yīng)的int類對(duì)象就是不可變的對(duì)象。Python解釋器認(rèn)為,這樣的不可變對(duì)象,只需要在內(nèi)存中存在一個(gè)就可以,因此,a和b指向同一個(gè)對(duì)象。而對(duì)于列表[1, 2, 3],由于列表是可變對(duì)象,即使這兩個(gè)對(duì)象的值相同,但它們不指向同一個(gè)對(duì)象。畢竟,誰也不知道后面的程序中會(huì)不會(huì)改變其中一個(gè)列表中的值。說到這里,或許能夠解釋Python的作者為什么要將Python的變量設(shè)計(jì)成只有引用式變量了,按照筆者粗淺的理解,這樣做的優(yōu)勢(shì)在于可以節(jié)約內(nèi)存。畢竟,Python為了能夠”簡(jiǎn)潔、優(yōu)雅“,為了能夠用一行代碼解決C語言用20行代碼才能解決的問題,在性能上犧牲了不少。
5、函數(shù)傳參機(jī)制
在Python中,函數(shù)傳參同樣傳遞的是對(duì)象的引用,函數(shù)參數(shù)是不可變對(duì)象時(shí),這沒有什么討論的價(jià)值。但是,倘若傳遞的參數(shù)是可變對(duì)象,如果你不注意這一點(diǎn),Bug可能就會(huì)默默地在凝視你,譬如:
def test1(a): a[-1] = 'end' a = [1, 2, 3] test1(a) print(a)
----運(yùn)行結(jié)果----
[1, 2, 'end']
可以看到,在運(yùn)行完函數(shù)test1后,a的值改變了,如果你不想讓他改變,這是Bug就來啦。
同樣,還有需要注意的一點(diǎn)是,不要把參數(shù)的默認(rèn)值設(shè)置成一個(gè)可變對(duì)象,否則Bug大概已經(jīng)在和你招手了:
# 用可變對(duì)象做參數(shù)默認(rèn)值帶來的bug # 例程來源于《流暢的Python》 class HauntedBus(): def __init__(self, passengers=[]): self.passengers = passengers def pick(self, name): # 乘客上車 self.passengers.append(name) def drop(self, name): # 乘客下車 self.passengers.remove(name) bus1 = HauntedBus(['zhang_san', 'li_si']) bus1.pick('wang_mazi') bus1.drop('zhang_san') print(bus1.passengers) bus2 = HauntedBus() bus2.pick('zhao_wu') print(bus2.passengers) bus3 = HauntedBus() print(bus3.passengers) print(bus2.passengers is bus3.passengers) print(bus3.passengers is bus1.passengers)
----運(yùn)行結(jié)果----
['li_si', 'wang_mazi']
['zhao_wu']
['zhao_wu']
True
False
你會(huì)驚奇地發(fā)現(xiàn),bus3.passengers難道不應(yīng)該是空列表嗎?這是因?yàn)?,HauntedBus的構(gòu)造函數(shù)中passengers的默認(rèn)值是一個(gè)可變對(duì)象,在對(duì)bus2進(jìn)行操作的時(shí)候,由于引用式變量的特性,改變了默認(rèn)值指向的可變對(duì)象。于是乎,就出現(xiàn)了意向不到的Bug。
6、擴(kuò)展閱讀
講到這里,其實(shí)本文的主要內(nèi)容就基本講完了。本節(jié)的內(nèi)容,除非說你需要開發(fā)自己的Python庫(kù),否則了解與否都基本不會(huì)影響你使用Python,你完全可以跳過本節(jié),完結(jié)撒花。
垃圾回收:在其他編程語言中都會(huì)討論變量或?qū)ο蟮纳嬷芷?,?huì)有垃圾回收機(jī)制,但在Python中好像很少談及這個(gè)問題。實(shí)際上,Python也存在垃圾回收機(jī)制,Python中每個(gè)變量都是對(duì)象的引用,如果某個(gè)對(duì)象不再被引用,這個(gè)對(duì)象就會(huì)被銷毀,這就是Python中的垃圾回收機(jī)制。del語句可以刪除變量,解除變量對(duì)對(duì)象的引用,如果這是對(duì)象的最后一個(gè)引用,這個(gè)對(duì)象就會(huì)被銷毀。
弱引用:弱引用不增加對(duì)象的引用數(shù),若對(duì)象存在,通過弱引用可以獲取對(duì)象。若對(duì)象已被銷毀,則弱引用返回None,這常用于緩存中。
最后,本文的目的在于幫助那些像我一樣從C語言轉(zhuǎn)移到Python的人,或者是被Python的變量、拷貝整得暈頭轉(zhuǎn)向的人。為了讓小白也有可能能看懂本文,我盡量寫得通俗易懂。但是限于本人水平,難免會(huì)有謬誤或疏漏之處,如有發(fā)現(xiàn),煩請(qǐng)?jiān)僭u(píng)論區(qū)指正,over。
總結(jié)
本篇文章就到這里了,希望能夠給你帶來幫助,也希望您能夠多多關(guān)注腳本之家的更多內(nèi)容!
相關(guān)文章
利用Python找出序列中出現(xiàn)最多的元素示例代碼
這篇文章主要給大家介紹了關(guān)于利用Python找出序列中出現(xiàn)最多的元素的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧。2017-12-12python requests 測(cè)試代理ip是否生效
這篇文章主要介紹了python requests 測(cè)試代理ip是否生效的相關(guān)資料,需要的朋友可以參考下2018-07-07python代碼如何實(shí)現(xiàn)切換中英文輸入法
這篇文章主要介紹了python代碼如何實(shí)現(xiàn)切換中英文輸入法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-11-11python第三方包安裝路徑site-packages下.libs作用詳解
這篇文章主要為大家介紹了python?第三方包安裝路徑?site-packages?下面的以?.libs?結(jié)尾的路徑作用詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-09-09在Python中使用MySQL--PyMySQL的基本使用方法
PyMySQL 是在 Python3.x 版本中用于連接 MySQL 服務(wù)器的一個(gè)庫(kù),Python2中則使用mysqldb。這篇文章主要介紹了在Python中使用MySQL--PyMySQL的基本使用,需要的朋友可以參考下2019-11-11Python 等分切分?jǐn)?shù)據(jù)及規(guī)則命名的實(shí)例代碼
這篇文章主要介紹了Python 等分切分?jǐn)?shù)據(jù)及規(guī)則命名的實(shí)例代碼,代碼簡(jiǎn)單易懂,非常不錯(cuò),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2019-08-08