姓名:劉慧林;學號: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語言中的指針崔列,通過它你可以在內存中找到對象。話不多說旺遮,先看圖:
左邊的圖表示的就是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)站逐步可視化地運行上面的代碼,甚至是本文中的所有代碼它匕,這能加深你的理解展融。
在這段代碼中,首先創(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