從內存的角度看Python中的變量

姓名:劉慧林;學號:21021210619峭弟;學院:電子工程學院

1判帮、前言

由于筆者并未系統(tǒng)地學習過Python,對Python某些底層的實現(xiàn)細節(jié)一概不清楚裳仆,以至于在實際使用的時候會寫出一些奇奇怪怪的Bug(沒錯,別人寫代碼孤钦,我寫B(tài)ug)歧斟,比如對象的某些屬性莫名奇妙地改變。究其原因偏形,是對Python中的變量機制存在一些誤解静袖,畢竟以前一直是用C語言居多。無奈俊扭,只能深入學習這一部分的知識队橙,并總結成此文。

閱讀本文萨惑,你可以:

  • 了解Python中變量的“儲存”機制捐康。

  • 了解Python中賦值、淺拷貝于深拷貝的區(qū)別和使用場景庸蔼。

  • 了解Python中的函數(shù)傳參形式解总。

當然,你需要一點基礎的編程和面向對象的知識才能看懂本文姐仅。

2花枫、引用式變量

相信學過Python的小伙伴都聽過這樣一句話:Python中一切皆是對象刻盐。這意味著,哪怕是Python中的基本數(shù)據(jù)類型劳翰,其本質上也是對象敦锌,例如對于一個int類型的變量a,你可以調用int類對象的方法來求a的絕對值:

a = -1
a.abs()
1

在這個例子中佳簸,可以說:a是int類的一個實例對象乙墙,其值是-1。當然溺蕉,這句話其實說的不對伶丐,因為a并不是一個對象,而是對象的引用疯特。這聽起來很奇怪哗魂,但事實就是如此。Python中的變量都是引用式變量漓雅,他并不像C/C++中的變量录别,儲存著具體的數(shù)據(jù)類型或對象,他像是C++中的引用邻吞。通俗的講组题,Python中的變量相當于對象的別名,如果你有C語言的基礎抱冷,可以把它理解為C語言中的指針崔列,通過它你可以在內存中找到對象。話不多說旺遮,先看圖:

2022_python_1.png

左邊的圖表示的就是C語言中的變量赵讯,變量相當于一個“盒子”,“盒子”里裝著值耿眉,右邊表示的就是Python中的引用式變量边翼,a和b都是列表對象[1, 2, 3]的別名,像是貼在[1, 2, 3]上的”標簽“鸣剪,順著這些”標簽“组底,解釋器可以在內存中找到他們對應的對象。你也許會問筐骇,這有啥區(qū)別债鸡,不都是變量嗎。還是先看代碼:

a = [1, 2, 3]
b = a
a[2] = 9
print(a)
print(b)

----運行結果----
[1, 2, 9]
[1, 2, 9]

意想不到的事情發(fā)生了铛纬,明明代碼只改變了a的值娘锁,為什么b也跟著變了呢?這是因為饺鹃,a莫秆、b都是列表的引用间雀,并不是實際的列表,上述代碼通過a這個”標簽“改變了內存中列表[1, 2 ,3]的值镊屎,于是乎惹挟,你順著b”標簽“找到的列表,當然是改變了的缝驳。再看代碼:

a = [1, 2, 3]
b = a
a = [1, 2, 9]
print(a)
print(b)

----運行結果----
[1, 2, 9]
[1, 2, 3]

在這個例程中连锯,我們把[1, 2, 9]賦值給了a,然后再輸出a和b用狱,此時a已經(jīng)發(fā)生變化运怖,而b沒有改變,a從列表[1, 2, 3]的引用變成了列表[1, 2, 9]的引用夏伊,列表[1, 2, 3]在內存中并未發(fā)生任何改變摇展,這就是b輸出的值不發(fā)生變化的原因。到這里溺忧,你應該可以理解上面說的:<u style="box-sizing: border-box;">a是int類的一個實例對象咏连,其值是-1</u>為什么是錯的了。這樣的賦值語句在Python中的應該這樣理解:創(chuàng)建一個int類對象-1鲁森,讓a作為-1的引用祟滴。當然,右邊的值是常量或是可變對象歌溉,解釋器都會做出不同的反應垄懂,這將在下文進一步講解⊥炊猓總之草慧,啰啰嗦嗦說了這么多,就是希望大家都能搞明白這個問題榜晦,核心就是一句話:Python中的變量都是引用式變量,變量存儲的不是值羽圃,而是引用乾胶。

3、賦值朽寞、淺拷貝與深拷貝

看完上一節(jié)识窿,肯定有人會問,如果Python中的賦值都是引用脑融,那我想創(chuàng)建一個變量的副本做備份怎么辦喻频?這在C語言中簡單的一句b=a就可以實現(xiàn)的需求在Python中如何實現(xiàn)?Python中提供了三種復制的方式肘迎,即:

  • 賦值:創(chuàng)建對象的引用甥温。

  • 淺拷貝:拷貝對象锻煌,但不拷貝對象內部的子對象。

  • 深拷貝:拷貝對象姻蚓,并且拷貝對象內部的子對象宋梧。

一如既往地先看代碼,畢竟代碼最能說明問題:

import copy
a = [1, 2, [3, 3 , 3], [4, 4]]
b = a # 賦值
c = a.copy() # 淺拷貝狰挡,調用對象的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)

----運行結果----
[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]]

為了更方便闡述加叁,這里我先給出這個例程中對象在內存中的變化情況倦沧,當然我更建議你自己去這個網(wǎng)站逐步可視化地運行上面的代碼,甚至是本文中的所有代碼它匕,這能加深你的理解展融。

2022_python_2.gif

在這段代碼中,首先創(chuàng)建了一個列表對象超凳,這個列表的第3愈污、4個元素也是列表對象,a是這個列表的引用轮傍,把a賦值給b暂雹,此時b也是同一個對象的引用,在內存中创夜,它們指向同一個對象杭跪,因此可以看到無論怎么通過a改變這個對象,a和b都是相同的驰吓。c則是對a的淺拷貝涧尿,解釋器新開辟了一塊內存,存儲了原列表的一個副本檬贰,但是由于是淺拷貝姑廉,對象內部的子對象沒有被拷貝。因此翁涤,這個副本列表的后面兩個元素依舊和原列表一樣桥言,是列表[3, 3 , 3]和[4, 4]的引用,在內存中指向同樣的對象葵礼。代碼中的改變2讓原列表的第三個元素變成了另一個列表[-3, -3 , -3]的引用号阿,但是這個副本列表的第三個元素還是[3, 3 , 3]的引用。改變3則修給了原列表第四個元素指向的列表中的一個元素鸳粉,因此打印c你會發(fā)現(xiàn)它指向的列表對應位置的元素也改變了扔涧。而對于d,d是a的深拷貝,解釋器新開辟了一塊內存枯夜,完全復制了原列表對象(包括子列表對象)放在這塊內存中弯汰。因此,d指向的對象和a指向的對象沒有任何關系卤档,無論怎么改變a指向的那個列表蝙泼,都不會影響d指向的列表。

看到這里劝枣,你應該知道如何實現(xiàn)本節(jié)開頭的需求了汤踏。

4、is的用法和id()函數(shù)

在Python中舔腾,每個對象都有各自的編號溪胶、類型和值,一個對象被創(chuàng)建以后稳诚,它的編號就不會改變哗脖,可以理解為對象在內存中的地址。id()函數(shù)可以獲取對象的編號扳还,在CPython解釋器中才避,這個編號就是對象在內存中的地址。is是一個雙目運算符氨距,運算結果是布爾變量桑逝,用來比較兩個對象的編號是否相同,準確的說俏让,可以用于比較兩個變量是否是同一個對象的引用楞遏。

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)

----運行結果----
2667871075272
2667871075272
2667871075208
True
False

顯然,a首昔、b是同一個對象的引用寡喝,而c是淺拷貝的副本,因此a和c引用的不是同一個對象勒奇,即使這兩個對象的值相等预鬓。不知你是否還記得,第1節(jié)中還提到在賦值語句中赊颠,右邊是可變對象與不可變對象格二,解釋器會由不同的操作,比如下面的代碼:

a = 5
b = 5
print(a is b)
c = [1, 2, 3]
d = [1, 2, 3]
print(c is d)

----運行結果----
True
False

對a巨税、b分別賦值為5蟋定,但是它們卻是同一個對象的引用粉臊,這是因為草添,5是一個常量,對應的int類對象就是不可變的對象扼仲。Python解釋器認為远寸,這樣的不可變對象抄淑,只需要在內存中存在一個就可以,因此驰后,a和b指向同一個對象肆资。而對于列表[1, 2, 3],由于列表是可變對象灶芝,即使這兩個對象的值相同郑原,但它們不指向同一個對象。畢竟夜涕,誰也不知道后面的程序中會不會改變其中一個列表中的值犯犁。說到這里,或許能夠解釋Python的作者為什么要將Python的變量設計成只有引用式變量了女器,按照筆者粗淺的理解酸役,這樣做的優(yōu)勢在于可以節(jié)約內存。畢竟驾胆,Python為了能夠”簡潔涣澡、優(yōu)雅“,為了能夠用一行代碼解決C語言用20行代碼才能解決的問題丧诺,在性能上犧牲了不少入桂。

5、函數(shù)傳參機制

在Python中锅必,函數(shù)傳參同樣傳遞的是對象的引用事格,函數(shù)參數(shù)是不可變對象時,這沒有什么討論的價值搞隐。但是驹愚,倘若傳遞的參數(shù)是可變對象,如果你不注意這一點劣纲,Bug可能就會默默地在凝視你逢捺,譬如:

def test1(a):
 a[-1] = 'end'

a = [1, 2, 3]
test1(a)
print(a)

----運行結果----
[1, 2, 'end']

可以看到,在運行完函數(shù)test1后癞季,a的值改變了劫瞳,如果你不想讓他改變,這是Bug就來啦绷柒。

同樣志于,還有需要注意的一點是,不要把參數(shù)的默認值設置成一個可變對象废睦,否則Bug大概已經(jīng)在和你招手了:

# 用可變對象做參數(shù)默認值帶來的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)

----運行結果----
['li_si', 'wang_mazi']
['zhao_wu']
['zhao_wu']
True
False

你會驚奇地發(fā)現(xiàn)伺绽,bus3.passengers難道不應該是空列表嗎?這是因為,HauntedBus的構造函數(shù)中passengers的默認值是一個可變對象奈应,在對bus2進行操作的時候澜掩,由于引用式變量的特性,改變了默認值指向的可變對象杖挣。于是乎肩榕,就出現(xiàn)了意向不到的Bug。

6惩妇、擴展閱讀

講到這里株汉,其實本文的主要內容就基本講完了。本節(jié)的內容歌殃,除非說你需要開發(fā)自己的Python庫郎逃,否則了解與否都基本不會影響你使用Python,你完全可以跳過本節(jié)挺份,完結撒花褒翰。

垃圾回收:在其他編程語言中都會討論變量或對象的生存周期,會有垃圾回收機制匀泊,但在Python中好像很少談及這個問題优训。實際上,Python也存在垃圾回收機制各聘,Python中每個變量都是對象的引用揣非,如果某個對象不再被引用,這個對象就會被銷毀躲因,這就是Python中的垃圾回收機制早敬。del語句可以刪除變量,解除變量對對象的引用大脉,如果這是對象的最后一個引用搞监,這個對象就會被銷毀。

弱引用:弱引用不增加對象的引用數(shù)镰矿,若對象存在琐驴,通過弱引用可以獲取對象。若對象已被銷毀秤标,則弱引用返回None绝淡,這常用于緩存中。

最后苍姜,本文的目的在于幫助那些像我一樣從C語言轉移到Python的人牢酵,或者是被Python的變量、拷貝整得暈頭轉向的人衙猪。為了讓小白也有可能能看懂本文馍乙,我盡量寫得通俗易懂玉罐。但是限于本人水平,難免會有謬誤或疏漏之處潘拨,如有發(fā)現(xiàn),煩請再評論區(qū)指正饶号,over铁追。

參考資料

Python語言參考手冊:https://docs.python.org/zh-cn/3/reference/datamodel.html

Luciano Ramalho:《流暢的Python》第八章,人民郵電出版社茫船。

Python 直接賦值琅束、淺拷貝和深度拷貝解析:https://www.runoob.com/w3cnote/python-understanding-dict-copy-shallow-or-deep.html

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市算谈,隨后出現(xiàn)的幾起案子涩禀,更是在濱河造成了極大的恐慌,老刑警劉巖然眼,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件艾船,死亡現(xiàn)場離奇詭異,居然都是意外死亡高每,警方通過查閱死者的電腦和手機屿岂,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來鲸匿,“玉大人爷怀,你說我怎么就攤上這事〈叮” “怎么了运授?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長乔煞。 經(jīng)常有香客問我吁朦,道長,這世上最難降的妖魔是什么渡贾? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任喇完,我火速辦了婚禮,結果婚禮上剥啤,老公的妹妹穿的比我還像新娘锦溪。我一直安慰自己,他們只是感情好府怯,可當我...
    茶點故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布刻诊。 她就那樣靜靜地躺著,像睡著了一般牺丙。 火紅的嫁衣襯著肌膚如雪则涯。 梳的紋絲不亂的頭發(fā)上复局,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天,我揣著相機與錄音粟判,去河邊找鬼亿昏。 笑死,一個胖子當著我的面吹牛档礁,可吹牛的內容都是我干的角钩。 我是一名探鬼主播,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼呻澜,長吁一口氣:“原來是場噩夢啊……” “哼递礼!你這毒婦竟也來了?” 一聲冷哼從身側響起羹幸,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤脊髓,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后栅受,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體将硝,經(jīng)...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年屏镊,在試婚紗的時候發(fā)現(xiàn)自己被綠了袋哼。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,785評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡闸衫,死狀恐怖涛贯,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情蔚出,我是刑警寧澤弟翘,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站骄酗,受9級特大地震影響稀余,放射性物質發(fā)生泄漏。R本人自食惡果不足惜趋翻,卻給世界環(huán)境...
    茶點故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一睛琳、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧踏烙,春花似錦师骗、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至荐捻,卻和暖如春黍少,著一層夾襖步出監(jiān)牢的瞬間寡夹,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工厂置, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留菩掏,地道東北人。 一個月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓昵济,卻偏偏與公主長得像智绸,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子砸紊,可洞房花燭夜當晚...
    茶點故事閱讀 44,713評論 2 354

推薦閱讀更多精彩內容