Python賦值邏輯的實現(xiàn)
摘要:
如果你學過 C 語言,那么當你初見 Python 時可能會覺得 Python 的賦值方式略有詭異:好像差不多,但又好像哪里有點不太對勁。
本文比較并解釋了這種賦值邏輯上的差異。回答了為什么需要這種賦值邏輯以及如何使用這種賦值邏輯的問題。
當然,即使未學過 C 語言,也可通過本文更好地了解 Python 的賦值邏輯——這種賦值邏輯影響著 Python 的方方面面,從而可以讓你更好地理解和編寫 Python 程序。
第一章 引例
先來看一組似乎矛盾的代碼:
# 代碼 1 >>> a = 3 >>> b = a >>> b = 5 >>> a 3
這看上去似乎很好理解。第二步中, a
只是把值復制給 b
,然后 b
又被更新為 5
,a
和 b
是兩個獨立的變量,那么 a
的值當然不會受到影響。
真的是這樣嗎?
再來看一段代碼:
# 代碼 2 >>> a = [1, 2, 3] >>> b = a >>> b[0] = 1024 >>> a [1024, 2, 3]
第二步中,a
只是復制把列表復制給 b
,然后更新 b[0]
的值,最后輸出 a
,可是 a
竟然也被改變了。
按照代碼 1 的邏輯(即變量之間獨立),代碼 2 的中的 a
不應該受到影響。
為什么出現(xiàn)了這樣的差異?
第二章 Python 的“反直覺”
先不解釋上面那個“看似矛盾”的問題。
先來看看另一組簡單的 Python 代碼在內(nèi)存中是什么樣子的:
# 代碼 3 b = 3 b = b + 5
它在內(nèi)存中的操作示意圖是這樣的:
然而,從代碼的的字面意思上看,“把 3 賦給 b,把 b 加 5 之后再賦給 b。”
也就是把代碼看成這個樣子:
b ← 3b ← b + 5
所以下面這張在內(nèi)存中的操作圖可能更符合我們的直覺:
也即 b + 5
的值又寫回到 b
中。典型的 C 程序就是這樣的。為變量 b
分配一個 int
型的內(nèi)存單元,然后將整數(shù) 3
存放在該內(nèi)存單元中。b
就代表了該塊內(nèi)存空間,不再移動,可以更新 b
的值,但 b
在內(nèi)存中的地址就不再變化了。所以我們說 b = b + 5
,就等于 b ← b + 5
,把 b
的值加 5
之后還依然放入 b
中。 變量 b
和它所在內(nèi)存空間緊緊綁定在一起,人形合一。
而再看看上面 Python 中的內(nèi)存示意圖,b + 5
得到了一個新值,然后令 b
指向了這個新值。換句話說,它做的是事情是這樣的:
b → 3 b → b + 5
先令 b
指向 3
,再令 b
指向 b + 5
這個新值。
C 程序更新的是內(nèi)存單元中存放的值,而 Python 更新的是變量的指向。
C 程序中變量保存了一個值,而 Python 中的變量指向一個值。
如果說 C 程序是通過操縱內(nèi)存地址而間接操作數(shù)據(jù)(每個變量固定對應一個內(nèi)存地址,所以說操縱變量就是操縱內(nèi)存地址),數(shù)據(jù)處于被動地位,那么 Python 則是直接操縱數(shù)據(jù),數(shù)據(jù)處于主動地位,變量只是作為一種引用關(guān)系而存在,而不再擁有存儲功能。
在 Python 中,每一個數(shù)據(jù)都會占用一個內(nèi)存空間,如 b + 5
這個新的數(shù)據(jù)也占用了一個全新的內(nèi)存空間。
Python 的這種操作讓數(shù)據(jù)成為主體,數(shù)據(jù)與數(shù)據(jù)之間直接進行交互。
而數(shù)據(jù)在 Python 中被稱為對象 (Object)。
這句話并不太嚴謹。不過在這個簡單的例子中是成立的。
一個整數(shù) 3
是一個 int
型對象,一個 'hello'
是一個字符串對象,一個 [1, 2, 3]
是一個列表對象。
Python 把一切數(shù)據(jù)都看成「對象」。它為每一個對象分配一個內(nèi)存空間。 一個對象被創(chuàng)建后,它的 id 就不再發(fā)生變化。
id 是 identity 的縮寫。意為“身份;標識”。
在 Python 中,可以使用id()
,來獲得一個對象的 id,可以看作是該對象在內(nèi)存中的地址。
一個對象被創(chuàng)建后,它不能被直接銷毀。因此,在上個例子中,變量 b
首先指向了對象 3
,然后繼續(xù)執(zhí)行 b + 5
,b + 5
產(chǎn)生了一個新的對象 8
,由于對象 3
不能被銷毀,則令 b
指向新的對象 8
,而不是用對象 8
去覆蓋對象 3
。在代碼執(zhí)行完成后,內(nèi)存中依然有對象 3
,也有對象 8
,變量 b
指向了對象 8
。
如果沒有變量指向?qū)ο?nbsp;3
(即無法引用它了),Python 會使用垃圾回收算法來決定是否回收它(這是自動的,不需要程序編寫者操心)。
一個舊的對象不能被覆蓋,因舊的對象交互而新產(chǎn)生的數(shù)據(jù)會放在新的對象中。也就是說每個對象是一個獨立的個體,每個對象都有自己的“主權(quán)”。因此,兩個對象的交互可以產(chǎn)生一個新的對象,而不會對原對象產(chǎn)生影響。在大型程序中,各個對象之間的交互錯綜復雜,這種獨立性則使得這些交互足夠安全。
C 程序為每個變量都分配一個了固定的內(nèi)存地址,這保證了 C 變量之間的獨立性。
C 語言是變量(也即內(nèi)存地址)之間的交互,Python 是對象(數(shù)據(jù))之間的交互。這是兩種不同的交互方式。
那么,Python 這種數(shù)據(jù)之間直接進行交互的好處體現(xiàn)在哪里?
很遺憾,這并不是本文所要討論的內(nèi)容,該部分屬于面向?qū)ο笤O(shè)計的核心內(nèi)容。本文只是對 Python 的這種交互方式與 C 語言的交互方式做了一些比較,以區(qū)分兩者在邏輯與物理上的差異所在。
相信這種邏輯會幫助你更好地編寫 Python 程序,并且?guī)椭阍谌蘸蟾由钊氲乩斫饷嫦驅(qū)ο蟮某绦蛟O(shè)計。
本章補充:
Python 的賦值更改的是變量的指向關(guān)系,因此,對于 Python,從前向后閱讀一個賦值表達式會更加容易理解。
// C 語言 b ← b + 5 // 把 b+5 的值賦給 b
# Python b → b + 5 # 令 b 指向 b + 5
第三章 回答第一章的問題
先看代碼 1:
# 代碼 1 >>> a = 3 >>> b = a >>> b = 5 >>> a 3
Python 中所有的數(shù)據(jù)都是對象,數(shù)字類型也不例外。3
是一個 int
類型的對象,5
也是一個 int
型的對象。
第一行,a
指向?qū)ο?nbsp;3
。
第二行,令 b
也指向 a
所指向的對象 3
。
第三行,因為對象不可被覆蓋(銷毀),令 b
指向新對象 5
,則只剩下 a
指向?qū)ο?nbsp;3
。
第四行,輸出 a
,得到 3
。
在內(nèi)存中的操作示意圖 (Python):
這與第一章中的解釋完全不同,第一章中的解釋是用 C 語言解釋的:
這是兩種完全不一樣的機制。
Python 中 b
首先指向了對象 3
,然而因為對象之間的獨立性,一個對象不能去覆蓋另一個對象,則令 b
指向?qū)ο?nbsp;5
,而不是將對象 3
在內(nèi)存中替換為對象 5
。
再來看代碼 2:
# 代碼 2 >>> a = [1, 2, 3] >>> b = a >>> b[0] = 1024 >>> a [1024, 2, 3]
第一行,令 a
指向一個列表 [1, 2, 3]
;
第二行,令 b
也指向 a
所指向的列表;
第三行,令 b[0] = 1024
,1024
雖然是一個對象,但它并沒有試圖覆蓋b
所指向的對象,而是對該對象的第一個元素進行修改。修改,而不是覆蓋,所以它可以原對象進行操作,而不是令 b
指向修改后的對象。
所在第四行輸出的 a
所指向的列表也發(fā)生了變化。
在內(nèi)存中的操作示意圖 (Python):
這種對象的值可以修改的對象被稱為可變對象 (immutable object)。常見的列表、字典為可變對象。
因為它的值可以被修改,因此如果有多個變量指向該列表:
a = [1, 2, 3] b = a c = a d = a ...
那么使用 b, c, d, ...
的任何一個變量都能訪問該對象并修改其中的內(nèi)容。這種特性常常被我們用于函數(shù)的參數(shù)傳遞,如果函數(shù)的參數(shù)是可變對象,那么函數(shù)可以對“實參”中的內(nèi)容進行修改:
>>> a = [1, 2, 3] >>> def change(t): t[0] = 1024 >>> change(a) >>> a [1024, 2, 3] >>>
調(diào)用函數(shù) change
時,令 t
也指向了 a
所指向的列表,然后使用 t
更改了列表中的第一個元素,更改,而不是覆蓋,因此對 t
所指向的對象的更改也改變了“實參” a
所指向的對象。而 C 語言則因為實參到形參是值傳遞,則無法改變實參的內(nèi)容(雖然借助指針可以實現(xiàn),但這里只說一般情況下)。
但在函數(shù)以外的區(qū)域,我們要盡量避免這樣使用,這很容易導致出錯(當然,有時候會很有用,這取決于你的程序)。比如,在多人協(xié)作編程時,如果甲不小心修改了某可變對象,那么乙、丙、丁等用到該對象的人都會受到影響。
而對于不可變對象 (immutable object),即其值無法更改的對象,傳入函數(shù)時則不會影響“實參”的值:
>>> a = 5 >>> def add(n): n = n + 2 >>> add(a) >>> a 5
調(diào)用函數(shù) add
時,令 n
也指向了 a
所指向的對象 5
, 再執(zhí)行 n = n + 2
,n
所指向的對象 5
與對象 2
相加得到了一個新的對象 7
,由于一個對象不能覆蓋另一個對象,則 n
指向新的對象 7
,而沒有改變原對象。因此 a
的值未發(fā)生變化。雖然與 C 程序的結(jié)果一致,但與 C 程序的機制完全不同,C 程序之所以沒改變 a
,是因為調(diào)用函數(shù)時只發(fā)生了值傳遞,即只把 a
的值復制給了 n
。
不要混淆這兩種賦值邏輯,它們有著完全不同的物理實現(xiàn)方式。
不同的思維邏輯會導致不同的編寫邏輯。盡管這兩種邏輯在很多情況下的結(jié)果是一致的,但并不能就簡單地認為它們是一致的。否則在一些小的細節(jié)方面出了錯誤,就會難以理解。只能死記硬背,把一些東西當作 Python 的特例來記,雖然「唯手熟爾」也可以讓你走得很遠,但思維正確時,不僅可以走得更遠,也會走得更加輕松。
比如,當你的思維清晰時,以下問題的答案自然也就水落石出了:
- 為什么列表的方法的返回值大多是
None
? - 為什么字符串的方法的返回值大多是一個新的對象?
- 為什么 Python 中沒有自增/自減運算符?
- 為什么有的可變對象傳入函數(shù)之后,卻不能被函數(shù)修改“實參”的值?
(比如將上面的change
函數(shù)的主體改成t = t[1:]
。調(diào)用函數(shù)之后,a
所指向的對象并沒有發(fā)生改變。) - ……
這些內(nèi)容與本文主題不大相關(guān),所以不再列出答案。
有趣的補充:
1. 數(shù)字是一個天然的不可變對象(immutable object)。
對于n = n + 2
,有人可能會說,為什么不能把它看成像列表那樣的修改,修改后n
依然指向的是原對象,這樣的話執(zhí)行add(a)
之后,a
就會變成7
了,可為什么不是這樣?
因為每一個數(shù)字都是一個單個的對象,而對象不能覆蓋對象。所以該句實際上是:a
指向的對象加上對象2
,產(chǎn)生了一個新的對象,然后令a
指向了新對象a + 2
。
因此,數(shù)字類型并不存在修改這一說,它是一個天然的不可變對象。2. 為什么 Python 中沒有自增(++)、自減(--)運算符?
自增或自減運算符,在 C 語言中很常用,簡潔實用。但在 Python 中卻一定不會有。上節(jié)說到,數(shù)字是天然的不可變對象,所謂自增就是自身增加,所以它無法自增。它只能從一個對象指向下一個對象。可以這樣寫a += 1
。
3. 既然 Python 更改的只是引用關(guān)系,那么如何復制一個列表?
# 答案: ## 1. 使用 list 的 copy 方法 b = a.copy() ## 2. 使用 slice 操作 b = a[:] # slice 操作返回一個新的對象
# 答案: ## 1. 使用 list 的 copy 方法 b = a.copy() ## 2. 使用 slice 操作 b = a[:] # slice 操作返回一個新的對象
到此這篇關(guān)于Python賦值邏輯的實現(xiàn)的文章就介紹到這了,更多相關(guān)Python賦值邏輯內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Python如何統(tǒng)計函數(shù)調(diào)用的耗時
這篇文章主要為大家詳細介紹了如何使用Python實現(xiàn)統(tǒng)計函數(shù)調(diào)用的耗時,文中的示例代碼講解詳細,感興趣的小伙伴可以跟隨小編一起學習一下2024-04-04pytorch中model.train()和model.eval()用法及說明
在PyTorch中,model.train()用于啟用BatchNormalization和Dropout,保證模型在訓練階段能夠有效地利用這些層的特性,而model.eval()則是用于測試階段,確保BatchNormalization和Dropout不會影響測試結(jié)果,保持模型的穩(wěn)定性2024-09-09簡析Python函數(shù)式編程字符串和元組及函數(shù)分類與高階函數(shù)
這篇文章主要介紹了Python函數(shù)式編程中的字符串、元組及函數(shù)分類與高階函數(shù),有需要的朋友可以借鑒參考下,希望能夠有所幫助2021-09-09如何用六步教會你使用python爬蟲爬取數(shù)據(jù)
網(wǎng)絡(luò)爬蟲就是按照一定規(guī)則自動訪問互聯(lián)網(wǎng)上的信息并把內(nèi)容下載下來的程序或腳本,下面這篇文章主要給大家介紹了關(guān)于如何用六步教會你使用python爬蟲爬取數(shù)據(jù)的相關(guān)資料,需要的朋友可以參考下2022-04-04