溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

Python賦值邏輯如何實(shí)現(xiàn)

發(fā)布時(shí)間:2023-02-22 11:39:23 來(lái)源:億速云 閱讀:105 作者:iii 欄目:開(kāi)發(fā)技術(shù)

本篇內(nèi)容介紹了“Python賦值邏輯如何實(shí)現(xiàn)”的有關(guān)知識(shí),在實(shí)際案例的操作過(guò)程中,不少人都會(huì)遇到這樣的困境,接下來(lái)就讓小編帶領(lǐng)大家學(xué)習(xí)一下如何處理這些情況吧!希望大家仔細(xì)閱讀,能夠?qū)W有所成!

第一章 引例

先來(lái)看一組似乎矛盾的代碼:

# 代碼 1
 
>>> a = 3
>>> b = a
>>> b = 5
>>> a
3

這看上去似乎很好理解。第二步中, a 只是把值復(fù)制給 b,然后 b 又被更新為 5,a 和 b 是兩個(gè)獨(dú)立的變量,那么 a 的值當(dāng)然不會(huì)受到影響。

真的是這樣嗎?

再來(lái)看一段代碼:

# 代碼 2
 
>>> a = [1, 2, 3]
>>> b = a
>>> b[0] = 1024
>>> a
[1024, 2, 3]

第二步中,a 只是復(fù)制把列表復(fù)制給 b,然后更新 b[0] 的值,最后輸出 a,可是 a 竟然也被改變了。

按照代碼 1 的邏輯(即變量之間獨(dú)立),代碼 2 的中的 a 不應(yīng)該受到影響。

為什么出現(xiàn)了這樣的差異?

第二章 Python 的“反直覺(jué)”

先不解釋上面那個(gè)“看似矛盾”的問(wèn)題。

先來(lái)看看另一組簡(jiǎn)單的 Python 代碼在內(nèi)存中是什么樣子的:

# 代碼 3
 
b = 3
b = b + 5

它在內(nèi)存中的操作示意圖是這樣的:

Python賦值邏輯如何實(shí)現(xiàn)

然而,從代碼的的字面意思上看,“把 3 賦給 b,把 b 加 5 之后再賦給 b?!?/p>

也就是把代碼看成這個(gè)樣子:

b ← 3b ← b + 5

所以下面這張?jiān)趦?nèi)存中的操作圖可能更符合我們的直覺(jué):

Python賦值邏輯如何實(shí)現(xiàn)

也即 b + 5 的值又寫(xiě)回到 b 中。典型的 C 程序就是這樣的。為變量 b 分配一個(gè) int 型的內(nèi)存單元,然后將整數(shù) 3 存放在該內(nèi)存單元中。b 就代表了該塊內(nèi)存空間,不再移動(dòng),可以更新 b 的值,但 b 在內(nèi)存中的地址就不再變化了。所以我們說(shuō) b = b + 5,就等于 b ← b + 5,把 b 的值加 5 之后還依然放入 b 中。 變量 b 和它所在內(nèi)存空間緊緊綁定在一起,人形合一。

而再看看上面 Python 中的內(nèi)存示意圖,b + 5 得到了一個(gè)新值,然后令 b 指向了這個(gè)新值。換句話說(shuō),它做的是事情是這樣的:

b → 3
b → b + 5

先令 b 指向 3,再令 b 指向 b + 5 這個(gè)新值。

C 程序更新的是內(nèi)存單元中存放的值,而 Python 更新的是變量的指向。
C 程序中變量保存了一個(gè)值,而 Python 中的變量指向一個(gè)值。

如果說(shuō) C 程序是通過(guò)操縱內(nèi)存地址而間接操作數(shù)據(jù)(每個(gè)變量固定對(duì)應(yīng)一個(gè)內(nèi)存地址,所以說(shuō)操縱變量就是操縱內(nèi)存地址),數(shù)據(jù)處于被動(dòng)地位,那么 Python 則是直接操縱數(shù)據(jù),數(shù)據(jù)處于主動(dòng)地位,變量只是作為一種引用關(guān)系而存在,而不再擁有存儲(chǔ)功能。

在 Python 中,每一個(gè)數(shù)據(jù)都會(huì)占用一個(gè)內(nèi)存空間,如 b + 5 這個(gè)新的數(shù)據(jù)也占用了一個(gè)全新的內(nèi)存空間。

Python 的這種操作讓數(shù)據(jù)成為主體,數(shù)據(jù)與數(shù)據(jù)之間直接進(jìn)行交互。

而數(shù)據(jù)在 Python 中被稱為對(duì)象 (Object)。

這句話并不太嚴(yán)謹(jǐn)。不過(guò)在這個(gè)簡(jiǎn)單的例子中是成立的。

一個(gè)整數(shù) 3 是一個(gè) int 型對(duì)象,一個(gè) 'hello' 是一個(gè)字符串對(duì)象,一個(gè) [1, 2, 3] 是一個(gè)列表對(duì)象。

Python 把一切數(shù)據(jù)都看成「對(duì)象」。它為每一個(gè)對(duì)象分配一個(gè)內(nèi)存空間。 一個(gè)對(duì)象被創(chuàng)建后,它的 id 就不再發(fā)生變化。

id 是 identity 的縮寫(xiě)。意為“身份;標(biāo)識(shí)”。
在 Python 中,可以使用 id(),來(lái)獲得一個(gè)對(duì)象的 id,可以看作是該對(duì)象在內(nèi)存中的地址。

一個(gè)對(duì)象被創(chuàng)建后,它不能被直接銷(xiāo)毀。因此,在上個(gè)例子中,變量 b 首先指向了對(duì)象 3,然后繼續(xù)執(zhí)行 b + 5,b + 5 產(chǎn)生了一個(gè)新的對(duì)象 8,由于對(duì)象 3 不能被銷(xiāo)毀,則令 b 指向新的對(duì)象 8,而不是用對(duì)象 8 去覆蓋對(duì)象 3。在代碼執(zhí)行完成后,內(nèi)存中依然有對(duì)象 3,也有對(duì)象 8,變量 b 指向了對(duì)象 8。

如果沒(méi)有變量指向?qū)ο?nbsp;3(即無(wú)法引用它了),Python 會(huì)使用垃圾回收算法來(lái)決定是否回收它(這是自動(dòng)的,不需要程序編寫(xiě)者操心)。

一個(gè)舊的對(duì)象不能被覆蓋,因舊的對(duì)象交互而新產(chǎn)生的數(shù)據(jù)會(huì)放在新的對(duì)象中。也就是說(shuō)每個(gè)對(duì)象是一個(gè)獨(dú)立的個(gè)體,每個(gè)對(duì)象都有自己的“主權(quán)”。因此,兩個(gè)對(duì)象的交互可以產(chǎn)生一個(gè)新的對(duì)象,而不會(huì)對(duì)原對(duì)象產(chǎn)生影響。在大型程序中,各個(gè)對(duì)象之間的交互錯(cuò)綜復(fù)雜,這種獨(dú)立性則使得這些交互足夠安全。

C 程序?yàn)槊總€(gè)變量都分配一個(gè)了固定的內(nèi)存地址,這保證了 C 變量之間的獨(dú)立性。

C 語(yǔ)言是變量(也即內(nèi)存地址)之間的交互,Python 是對(duì)象(數(shù)據(jù))之間的交互。這是兩種不同的交互方式。

那么,Python 這種數(shù)據(jù)之間直接進(jìn)行交互的好處體現(xiàn)在哪里?

很遺憾,這并不是本文所要討論的內(nèi)容,該部分屬于面向?qū)ο笤O(shè)計(jì)的核心內(nèi)容。本文只是對(duì) Python 的這種交互方式與 C 語(yǔ)言的交互方式做了一些比較,以區(qū)分兩者在邏輯與物理上的差異所在。

相信這種邏輯會(huì)幫助你更好地編寫(xiě) Python 程序,并且?guī)椭阍谌蘸蟾由钊氲乩斫饷嫦驅(qū)ο蟮某绦蛟O(shè)計(jì)。

本章補(bǔ)充:
Python 的賦值更改的是變量的指向關(guān)系,因此,對(duì)于 Python,從前向后閱讀一個(gè)賦值表達(dá)式會(huì)更加容易理解。

// C 語(yǔ)言
b ← b + 5	// 把 b+5 的值賦給 b
# Python
b → b + 5	# 令 b 指向 b + 5

第三章 回答第一章的問(wèn)題

先看代碼 1:

# 代碼 1
 
>>> a = 3
>>> b = a
>>> b = 5
>>> a
3

Python 中所有的數(shù)據(jù)都是對(duì)象,數(shù)字類(lèi)型也不例外。3 是一個(gè) int 類(lèi)型的對(duì)象,5 也是一個(gè) int 型的對(duì)象。
第一行,a 指向?qū)ο?nbsp;3
第二行,令 b 也指向 a 所指向的對(duì)象 3。
第三行,因?yàn)閷?duì)象不可被覆蓋(銷(xiāo)毀),令 b 指向新對(duì)象 5,則只剩下 a 指向?qū)ο?nbsp;3。
第四行,輸出 a,得到 3

在內(nèi)存中的操作示意圖 (Python):

Python賦值邏輯如何實(shí)現(xiàn)

這與第一章中的解釋完全不同,第一章中的解釋是用 C 語(yǔ)言解釋的:

Python賦值邏輯如何實(shí)現(xiàn)

這是兩種完全不一樣的機(jī)制。

Python 中 b 首先指向了對(duì)象 3,然而因?yàn)閷?duì)象之間的獨(dú)立性,一個(gè)對(duì)象不能去覆蓋另一個(gè)對(duì)象,則令 b 指向?qū)ο?nbsp;5,而不是將對(duì)象 3 在內(nèi)存中替換為對(duì)象 5。

再來(lái)看代碼 2:

# 代碼 2
 
>>> a = [1, 2, 3]
>>> b = a
>>> b[0] = 1024
>>> a
[1024, 2, 3]

第一行,令 a 指向一個(gè)列表 [1, 2, 3];
第二行,令 b 也指向 a 所指向的列表;
第三行,令 b[0] = 1024,1024 雖然是一個(gè)對(duì)象,但它并沒(méi)有試圖覆蓋b所指向的對(duì)象,而是對(duì)該對(duì)象的第一個(gè)元素進(jìn)行修改。修改,而不是覆蓋,所以它可以原對(duì)象進(jìn)行操作,而不是令 b 指向修改后的對(duì)象。
所在第四行輸出的 a 所指向的列表也發(fā)生了變化。

在內(nèi)存中的操作示意圖 (Python):

Python賦值邏輯如何實(shí)現(xiàn)

這種對(duì)象的值可以修改的對(duì)象被稱為可變對(duì)象 (immutable object)。常見(jiàn)的列表、字典為可變對(duì)象。

因?yàn)樗闹悼梢员恍薷?,因此如果有多個(gè)變量指向該列表:

a = [1, 2, 3]
b = a
c = a
d = a
...

那么使用 b, c, d, ... 的任何一個(gè)變量都能訪問(wèn)該對(duì)象并修改其中的內(nèi)容。這種特性常常被我們用于函數(shù)的參數(shù)傳遞,如果函數(shù)的參數(shù)是可變對(duì)象,那么函數(shù)可以對(duì)“實(shí)參”中的內(nèi)容進(jìn)行修改:

>>> a = [1, 2, 3]
>>> def change(t):
		t[0] = 1024
 
>>> change(a)
>>> a
[1024, 2, 3]
>>>

調(diào)用函數(shù) change 時(shí),令 t 也指向了 a 所指向的列表,然后使用 t 更改了列表中的第一個(gè)元素,更改,而不是覆蓋,因此對(duì) t 所指向的對(duì)象的更改也改變了“實(shí)參” a 所指向的對(duì)象。而 C 語(yǔ)言則因?yàn)閷?shí)參到形參是值傳遞,則無(wú)法改變實(shí)參的內(nèi)容(雖然借助指針可以實(shí)現(xiàn),但這里只說(shuō)一般情況下)。

但在函數(shù)以外的區(qū)域,我們要盡量避免這樣使用,這很容易導(dǎo)致出錯(cuò)(當(dāng)然,有時(shí)候會(huì)很有用,這取決于你的程序)。比如,在多人協(xié)作編程時(shí),如果甲不小心修改了某可變對(duì)象,那么乙、丙、丁等用到該對(duì)象的人都會(huì)受到影響。

而對(duì)于不可變對(duì)象 (immutable object),即其值無(wú)法更改的對(duì)象,傳入函數(shù)時(shí)則不會(huì)影響“實(shí)參”的值:

>>> a = 5
>>> def add(n):
		n = n + 2
 
>>> add(a)
>>> a
5

調(diào)用函數(shù) add 時(shí),令 n 也指向了 a 所指向的對(duì)象 5, 再執(zhí)行 n = n + 2n 所指向的對(duì)象 5 與對(duì)象 2 相加得到了一個(gè)新的對(duì)象 7,由于一個(gè)對(duì)象不能覆蓋另一個(gè)對(duì)象,則 n 指向新的對(duì)象 7,而沒(méi)有改變?cè)瓕?duì)象。因此 a 的值未發(fā)生變化。雖然與 C 程序的結(jié)果一致,但與 C 程序的機(jī)制完全不同,C 程序之所以沒(méi)改變 a,是因?yàn)檎{(diào)用函數(shù)時(shí)只發(fā)生了值傳遞,即只把 a 的值復(fù)制給了 n。

不要混淆這兩種賦值邏輯,它們有著完全不同的物理實(shí)現(xiàn)方式。

不同的思維邏輯會(huì)導(dǎo)致不同的編寫(xiě)邏輯。盡管這兩種邏輯在很多情況下的結(jié)果是一致的,但并不能就簡(jiǎn)單地認(rèn)為它們是一致的。否則在一些小的細(xì)節(jié)方面出了錯(cuò)誤,就會(huì)難以理解。只能死記硬背,把一些東西當(dāng)作 Python 的特例來(lái)記,雖然「唯手熟爾」也可以讓你走得很遠(yuǎn),但思維正確時(shí),不僅可以走得更遠(yuǎn),也會(huì)走得更加輕松。

比如,當(dāng)你的思維清晰時(shí),以下問(wèn)題的答案自然也就水落石出了:

  • 為什么列表的方法的返回值大多是 None?

  • 為什么字符串的方法的返回值大多是一個(gè)新的對(duì)象?

  • 為什么 Python 中沒(méi)有自增/自減運(yùn)算符?

  • 為什么有的可變對(duì)象傳入函數(shù)之后,卻不能被函數(shù)修改“實(shí)參”的值?
    (比如將上面的 change 函數(shù)的主體改成 t = t[1:]。調(diào)用函數(shù)之后,a 所指向的對(duì)象并沒(méi)有發(fā)生改變。)

  • ……

這些內(nèi)容與本文主題不大相關(guān),所以不再列出答案。

有趣的補(bǔ)充:

1. 數(shù)字是一個(gè)天然的不可變對(duì)象(immutable object)。
對(duì)于 n = n + 2,有人可能會(huì)說(shuō),為什么不能把它看成像列表那樣的修改,修改后 n 依然指向的是原對(duì)象,這樣的話執(zhí)行 add(a) 之后,a 就會(huì)變成 7 了,可為什么不是這樣?
因?yàn)槊恳粋€(gè)數(shù)字都是一個(gè)單個(gè)的對(duì)象,而對(duì)象不能覆蓋對(duì)象。所以該句實(shí)際上是: a 指向的對(duì)象加上對(duì)象 2,產(chǎn)生了一個(gè)新的對(duì)象,然后令 a 指向了新對(duì)象 a + 2。
因此,數(shù)字類(lèi)型并不存在修改這一說(shuō),它是一個(gè)天然的不可變對(duì)象。

2. 為什么 Python 中沒(méi)有自增(++)、自減(--)運(yùn)算符?
自增或自減運(yùn)算符,在 C 語(yǔ)言中很常用,簡(jiǎn)潔實(shí)用。但在 Python 中卻一定不會(huì)有。上節(jié)說(shuō)到,數(shù)字是天然的不可變對(duì)象,所謂自增就是自身增加,所以它無(wú)法自增。它只能從一個(gè)對(duì)象指向下一個(gè)對(duì)象??梢赃@樣寫(xiě) a += 1。
3. 既然 Python 更改的只是引用關(guān)系,那么如何復(fù)制一個(gè)列表?

# 答案:
## 1. 使用 list 的 copy 方法
b = a.copy()
## 2. 使用 slice 操作
b = a[:]	# slice 操作返回一個(gè)新的對(duì)象
# 答案:
## 1. 使用 list 的 copy 方法
b = a.copy()
## 2. 使用 slice 操作
b = a[:]	# slice 操作返回一個(gè)新的對(duì)象

“Python賦值邏輯如何實(shí)現(xiàn)”的內(nèi)容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業(yè)相關(guān)的知識(shí)可以關(guān)注億速云網(wǎng)站,小編將為大家輸出更多高質(zhì)量的實(shí)用文章!

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI