日本免费高清视频-国产福利视频导航-黄色在线播放国产-天天操天天操天天操天天操|www.shdianci.com

學無先后,達者為師

網(wǎng)站首頁 編程語言 正文

帶你從內(nèi)存的角度看Python中的變量_python

作者:菜雞劉 ? 更新時間: 2022-03-27 編程語言

1、前言

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

閱讀本文,你可以:

  • 了解Python中變量的“儲存”機制。
  • 了解Python中賦值、淺拷貝于深拷貝的區(qū)別和使用場景。
  • 了解Python中的函數(shù)傳參形式。

當然,你需要一點基礎的編程和面向?qū)ο蟮闹R才能看懂本文。

2、引用式變量

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

>>> a = -1
>>> a.__abs__()
1

在這個例子中,可以說:a是int類的一個實例對象,其值是-1。當然,這句話其實說的不對,因為a并不是一個對象,而是對象的引用。這聽起來很奇怪,但事實就是如此。Python中的變量都是引用式變量,他并不像C/C++中的變量,儲存著具體的數(shù)據(jù)類型或?qū)ο螅袷荂++中的引用。通俗的講,Python中的變量相當于對象的別名,如果你有C語言的基礎,可以把它理解為C語言中的指針,通過它你可以在內(nèi)存中找到對象。話不多說,先看圖:

圖片來源:《流暢的Python》8.1

左邊的圖表示的就是C語言中的變量,變量相當于一個“盒子”,“盒子”里裝著值,右邊表示的就是Python中的引用式變量,a和b都是列表對象[1, 2, 3]的別名,像是貼在[1, 2, 3]上的”標簽“,順著這些”標簽“,解釋器可以在內(nèi)存中找到他們對應的對象。你也許會問,這有啥區(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這個”標簽“改變了內(nèi)存中列表[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]在內(nèi)存中并未發(fā)生任何改變,這就是b輸出的值不發(fā)生變化的原因。到這里,你應該可以理解上面說的:a是int類的一個實例對象,其值是-1為什么是錯的了。這樣的賦值語句在Python中的應該這樣理解:創(chuàng)建一個int類對象-1,讓a作為-1的引用。當然,右邊的值是常量或是可變對象,解釋器都會做出不同的反應,這將在下文進一步講解。總之,啰啰嗦嗦說了這么多,就是希望大家都能搞明白這個問題,核心就是一句話:Python中的變量都是引用式變量,變量存儲的不是值,而是引用。

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

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

  • 賦值:創(chuàng)建對象的引用。
  • 淺拷貝:拷貝對象,但不拷貝對象內(nèi)部的子對象。
  • 深拷貝:拷貝對象,并且拷貝對象內(nèi)部的子對象。

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

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

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

在這里插入圖片描述

在這段代碼中,首先創(chuàng)建了一個列表對象,這個列表的第3、4個元素也是列表對象,a是這個列表的引用,把a賦值給b,此時b也是同一個對象的引用,在內(nèi)存中,它們指向同一個對象,因此可以看到無論怎么通過a改變這個對象,a和b都是相同的。c則是對a的淺拷貝,解釋器新開辟了一塊內(nèi)存,存儲了原列表的一個副本,但是由于是淺拷貝,對象內(nèi)部的子對象沒有被拷貝。因此,這個副本列表的后面兩個元素依舊和原列表一樣,是列表[3, 3 , 3]和[4, 4]的引用,在內(nèi)存中指向同樣的對象。代碼中的改變2讓原列表的第三個元素變成了另一個列表[-3, -3 , -3]的引用,但是這個副本列表的第三個元素還是[3, 3 , 3]的引用。改變3則修給了原列表第四個元素指向的列表中的一個元素,因此打印c你會發(fā)現(xiàn)它指向的列表對應位置的元素也改變了。而對于d,d是a的深拷貝,解釋器新開辟了一塊內(nèi)存,完全復制了原列表對象(包括子列表對象)放在這塊內(nèi)存中。因此,d指向的對象和a指向的對象沒有任何關系,無論怎么改變a指向的那個列表,都不會影響d指向的列表。

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

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

在Python中,每個對象都有各自的編號、類型和值,一個對象被創(chuàng)建以后,它的編號就不會改變,可以理解為對象在內(nèi)存中的地址。id()函數(shù)可以獲取對象的編號,在CPython解釋器中,這個編號就是對象在內(nèi)存中的地址。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解釋器認為,這樣的不可變對象,只需要在內(nèi)存中存在一個就可以,因此,a和b指向同一個對象。而對于列表[1, 2, 3],由于列表是可變對象,即使這兩個對象的值相同,但它們不指向同一個對象。畢竟,誰也不知道后面的程序中會不會改變其中一個列表中的值。說到這里,或許能夠解釋Python的作者為什么要將Python的變量設計成只有引用式變量了,按照筆者粗淺的理解,這樣做的優(yōu)勢在于可以節(jié)約內(nèi)存。畢竟,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、擴展閱讀

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

垃圾回收:在其他編程語言中都會討論變量或?qū)ο蟮纳嬷芷冢瑫欣厥諜C制,但在Python中好像很少談及這個問題。實際上,Python也存在垃圾回收機制,Python中每個變量都是對象的引用,如果某個對象不再被引用,這個對象就會被銷毀,這就是Python中的垃圾回收機制。del語句可以刪除變量,解除變量對對象的引用,如果這是對象的最后一個引用,這個對象就會被銷毀。

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

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

總結

原文鏈接:https://blog.csdn.net/forGemini/article/details/122420790

欄目分類
最近更新