Python 世界的黑客帝國

文 |?太陽雪
來源:Python 技術(shù)「ID: pythonall」

相比于子彈時間和火爆場景,我更喜歡《黑客帝國》故事背景的假設(shè) —— 人們熟悉的世界是虛構(gòu)的,是機(jī)器給人大腦輸入的幻象,而幻象是不完美的,存在一些不符合自然規(guī)律的地方,這些地方或多或少的展示了幻象世界的破綻和真實(shí)世界的樣子,如果你看過《黑客帝國》動畫版《超越極限》和《世界紀(jì)錄》,將會有更深刻的感受
我們熟悉的、賴以生存的 Python 世界,其實(shí)也是個虛擬的,這個虛擬世界展示給我們無比絢爛的場景和功能的同時,也存在一些超乎常理和認(rèn)知的地方,今天就帶你一起探尋一些那些超自然點(diǎn),以及它背后的真實(shí)世界
神奇的海象操作符
海象操作符 := 是 Python3.8 引入的一個新特性,意為節(jié)省一次臨時變量的賦值操作,例如:
a?=?[1,2,3,4,5]
n?=?len(a)
if?n?>?4:
????print(n)
意思是,如果列表 a 的長度大于 4,則打印 a 的長度,為了避免對列表長度的兩次求解,利用變量 n 存儲 a 的長度,合情合理
如果用 海象操作符(:=),會是這樣:
a?=?[1,2,3,4,5]
if?n?:=?len(n)?>?4:
????print(n)
可以看到,省去了臨時變量 n 的定義,通過海象操作符,一舉兩得
不得不說,Python 為能讓我們提高效率,真是挖空心思,剛剛發(fā)布的 正式版 Python3.9,也是為提升效率做了多處改善
海象的表演
不過,看下面的代碼
>>>?a?=?"wtf_walrus"
>>>?a
'wtf_walrus'
>>>?a?:=?"wtf_walrus"??#?報錯!
??File?"" ,?line?1
????a?:=?'wtf_walrus'
??????^
SyntaxError:?invalid?syntax
>>>?(a?:=?"wtf_walrus")?#?奇跡發(fā)生,竟然通過了!
'wtf_walrus'
>>>?a
'wtf_walrus'
再來一段
>>>?a?=?6,?9??#?元組賦值
>>>?a??#?結(jié)果正常
(6,?9)
>>>?(a?:=?6,?9)??#?海象賦值,表達(dá)式結(jié)果正常
(6,?9)
>>>?a??#?臨時變量竟然不同
6
>>>?a,?b?=?6,?9?#?解包賦值
>>>?a,?b
(6,?9)
>>>?(a,?b?=?16,?19)?# Oh no!
??File?"" ,?line?1
????(a,?b?=?6,?9)
??????????^
SyntaxError:?invalid?syntax
>>>?(a,?b?:=?16,?19)?#?這里竟然打印出三員元組!
(6,?16,?19)
>>>?a?#?問題是?a?竟然沒變
6
>>>?b
16
解密海象
非括號表達(dá)式的海象賦值操作海象操作符(
:=)適用于一個表達(dá)式內(nèi)部的作用域,沒有括號,相當(dāng)于全局作業(yè)域,是會受到編譯器限制的括號里的賦值操作相應(yīng)的,賦值操作符(
=)不能放在括號里,因?yàn)樗枰谌肿饔糜蛑袌?zhí)行,而非在一個表達(dá)式內(nèi)海象操作符的本質(zhì)海象操作符的語法形式為
Name := expr,Name為正常的標(biāo)識符,expr為正常的表達(dá)式,因此可迭代的裝包和解包表現(xiàn)的結(jié)果會和期望不同(a := 6, 9)實(shí)際上會被解析為((a := 6), 9),最終,a 的值為 6,驗(yàn)證一下:>>>?(a?:=?6,?9)?==?((a?:=?6),?9)
True
>>>?x?=?(a?:=?696,?9)
>>>?x
(696,?9)
>>>?x[0]?is?a?#?引用了相同的值
True同樣的,
(a, b := 16, 19) 相當(dāng)于(a, (b := 16), 19,原來如此
不安分的字符串
先建立一個感知認(rèn)識
>>>?a?=?"some_string"
>>>?id(a)
140420665652016
>>>?id("some"?+?"_"?+?"string")?#?不同方式創(chuàng)建的字符串實(shí)質(zhì)是一樣的.
140420665652016
奇特的事情即將發(fā)生
>>>?a?=?"wtf"
>>>?b?=?"wtf"
>>>?a?is?b
True
>>>?a?=?"wtf!"
>>>?b?=?"wtf!"
>>>?a?is?b??#?什么鬼!
False
如果將這段代碼寫入腳本文件,用 Python 運(yùn)行,結(jié)果卻是對的:
a?=?"wtf!"
b?=?"wtf!"
print(a?is?b)??#?將打印出?True
還有更神奇的,在 Python3.7 之前的版本中,會有下面的現(xiàn)象:
>>>?'a'?*?20?is?'aaaaaaaaaaaaaaaaaaaa'
True
>>>?'a'?*?21?is?'aaaaaaaaaaaaaaaaaaaaa'
False
20 個字符 a 組合起來等于 20個 a 的字符串,而 21 個就不相等

揭秘字符串
計(jì)算機(jī)世界里,任何奇特的現(xiàn)象都有其必然原因
這些字符串行為,是由于 Cpython 在編譯優(yōu)化時,某些情況下會對不可變的對象做存儲,新建時之間建立引用,而不是創(chuàng)建新的,這種技術(shù)被稱作 字符串駐留(string interning),這樣做可以節(jié)省內(nèi)存和提高效率
上面代碼中,字符串被隱式駐留了,什么情況下才會被駐留呢?
所有長度為 0 和 1 的字符串都會被駐留 字符串在編譯時被駐留,運(yùn)算中不會( "wtf"會駐留,"".join("w", "t", "f")則不會)只有包含了字母、數(shù)值和下劃線的字符串才會被駐留,這就是為什么 "wtf!"不會被駐留的原因(其中還有字符!)如果 a 和 b 的賦值 "wtf!" 語句在同一行,Python 解釋器會創(chuàng)建一個對象,然后讓兩個變量指向這個對象。如果在不同行,解釋器就不知道已經(jīng)有了
"wtf!"對象,所以會創(chuàng)建新的(原因是"wtf!"不會被駐留)像 IPython 這樣的交互環(huán)境中,語句是單行執(zhí)行的,而腳本文件中,代碼是被同時編譯的,具有相同的編譯環(huán)境,所以就能理解,代碼文件中不同行的不被駐留字符串引用同一個對象的現(xiàn)象
常量折疊(constant folding)是 Python 的一個優(yōu)化技術(shù):窺孔優(yōu)化(peephole optimization),如
a = "a"*20,這樣的語句會在編譯時展開,以減少運(yùn)行時的運(yùn)行消耗,為了不至于讓 pyc 文件過大,將展開字符限制在 20 個以內(nèi),不然想想這個"a"*10**100語句將產(chǎn)生多大的 pyc 文件注意 Python3.7 以后的版本中 常量折疊 的問題得到了改善,不過還不清楚具體原因(矩陣變的更復(fù)雜了)
小心鏈?zhǔn)讲僮?/h2>
來一段騷操作
>>>?(False?==?False)?in?[False]?#?合乎常理
False
>>>?False?==?(False?in?[False])?#?也沒問題
False
>>>?False?==?False?in?[False]?#?現(xiàn)在感覺如何?
True
>>>?True?is?False?==?False
False
>>>?False?is?False?is?False
True
>>>?1?>?0?1
True
>>>?(1?>?0)?1
False
>>>?1?>?(0?1)
False
不知到你看到這段代碼的感受,反正我看第一次到時,懷疑我學(xué)的 Python 是假冒的~
到底發(fā)生了什么
按照 Python 官方文檔,表達(dá)式章節(jié),值比較小節(jié)的描述(https://docs.python.org/2/reference/expressions.html#not-in):
通常情況下,如果 a、b、c、...、y、z 是表達(dá)式,op1、op2、...、opN 是比較運(yùn)算符,那么
a op1 b op2 c ... y opN z等價于a op1 b and b op2 c and ... y opN z,除了每個表達(dá)式只被計(jì)算一次的特性
基于以上認(rèn)知,我們重新審視一下上面看起來讓人迷惑的語句:
False is False is False等價于(False is False) and (False is False)True is False == False等價于True is False and False == False,現(xiàn)在可以看出,第一部分True is False的求值為False, 所以整個表達(dá)式的值為False1 > 0 < 1等價于1 > 0 and 0 < 1,所以表達(dá)式求值為True表達(dá)式 (1 > 0) < 1等價于True < 1,另外int(True)的值為 1,且True + 1的值為 2,那么1 < 1就是False了
到底 is(是) 也不 is(是)
我直接被下面的代碼驚到了
>>>?a?=?256
>>>?b?=?256
>>>?a?is?b
True
>>>?a?=?257
>>>?b?=?257
>>>?a?is?b
False
再來一個
>>>?a?=?[]
>>>?b?=?[]
>>>?a?is?b
False
>>>?a?=?tuple()
>>>?b?=?tuple()
>>>?a?is?b
True
同樣是數(shù)字,同樣是對象,待遇咋就不一樣尼……
我們逐一理解下
is 和 == 的區(qū)別
is操作符用于檢查兩個變量引用的是否同一個對象實(shí)例==操作符用于檢查兩個變量引用對象的值是否相等所以
is用于引用比較,==用于值比較,下面的代碼能更清楚的說明這一點(diǎn):>>>?class?A:?pass
>>>?A()?is?A()??#?由于兩個對象實(shí)例在不同的內(nèi)存空間里,所以表達(dá)式值為?False
False
256 是既存對象,而 257 不是
這個小標(biāo)題讓人很無語,還確實(shí)對象和對象不一樣
在 Python 里 從 -5 到 256 范圍的數(shù)值,是預(yù)先初始化好的,如果值為這個范圍內(nèi)的數(shù)字,會直接建立引用,否則就會創(chuàng)建
這就解釋了為啥 同樣的 257,內(nèi)存對象不同的現(xiàn)象了
為什么要這樣做?官方的解釋為,這個范圍的數(shù)值比較常用(https://docs.python.org/3/c-api/long.html)
不可變的空元組
和 -5 到 256 數(shù)值預(yù)先創(chuàng)建一樣,對于不可變的對象,Python 解釋器也做了預(yù)先創(chuàng)建,例如 對空的 Tuple 對象
這就能解釋為什么 空列表對象之間引用不同,而空元組之間的引用確實(shí)相同的現(xiàn)象了
被吞噬的 Javascript
先看看過程
some_dict?=?{}
some_dict[5.5]?=?"Ruby"
some_dict[5.0]?=?"JavaScript"
some_dict[5]?=?"Python"
print(some_dict[5.5])??#?Ruby
print(some_dict[5.0])??# Python Javascript 去哪了?
背后的原因
Python 字典對象的索引,是通過鍵值是否相等和比較鍵的哈希值來進(jìn)行查找的
具有相同值的不可變對象的哈希值是相同的
>>>?5?==?5.0
True
>>>?hash(5)?==?hash(5.0)
True于是我們就能理解,當(dāng)執(zhí)行
some_dict[5] = "Python"時,會覆蓋掉前面定義的some_dict[5.0] = "Javascript",因?yàn)?5 和 5.0 具有相同的哈希值
需要注意的是:有可能不同的值具有相同的哈希值,這種現(xiàn)象被稱作 哈希沖突
關(guān)于字典對象使用哈希值作為索引運(yùn)算的更深層次的原因,有興趣的同學(xué)可以參考 StackOverflow 上的回答,解釋的很精彩,網(wǎng)址是:https://stackoverflow.com/questions/32209155/why-can-a-floating-point-dictionary-key-overwrite-an-integer-key-with-the-same-v/32211042
總結(jié)
限于篇幅(精力)原因,今天就介紹這幾個 Python 宇宙中的異?,F(xiàn)象,更多的異常現(xiàn)象,收錄在 satwikkansal 的 wtfpython 中(https://github.com/satwikkansal/wtfpython)。
任何華麗美好的背后都是各種智慧、技巧、妥協(xié)、辛苦的支撐,不是有那么一句話嘛:如果你覺得輕松自如,比如有人在負(fù)重前行。而這個人就是我們喜愛的 Python 及其 編譯器,要更好的理解一個東西,需要了解它背后的概念原理和理念,期望通過這篇短文對你有所啟發(fā)
本文代碼測試版本為 Python3.8.2,實(shí)踐中遇到問題,可留言或在交流群討論
參考
https://github.com/satwikkansal/wtfpython https://docs.python.org/2/reference/expressions.html#not-in https://docs.python.org/3/c-api/long.html https://github.com/leisurelicht/wtfpython-cn
PS:公號內(nèi)回復(fù)「Python」即可進(jìn)入Python 新手學(xué)習(xí)交流群,一起 100 天計(jì)劃!
老規(guī)矩,兄弟們還記得么,右下角的 “在看” 點(diǎn)一下,如果感覺文章內(nèi)容不錯的話,記得分享朋友圈讓更多的人知道!


【代碼獲取方式】
