Python為什麼直接運行和在命令行運行同樣語句但結果卻不同,他們的緩存機制不同嗎?

如圖,都是同樣的代碼,但是輸出結果卻不同,請大神指點。

禮貌貼上代碼。

a = 10.0
b = 10.0
print(a is b)


答案放在最前面:

對於Python而言,存儲好的腳本文件(Script file)和在Console中的互動式(interactive)命令,執行方式不同。對於腳本文件,解釋器將其當作整個代碼塊執行,而對於交互性命令行中的每一條命令,解釋器將其當作單獨的代碼塊執行。而Python在執行同一個代碼塊的初始化對象的命令時,會檢查是否其值是否已經存在,如果存在,會將其重用(這句話不夠嚴謹,後面會詳談)。所以在你給出的例子中,文件執行時(同一個代碼塊)會把a、b兩個變數指向同一個對象;而在命令行執行時,a、b賦值語句分別被當作兩個代碼塊執行,所以會得到兩個不同的對象,因而is判斷返回False。

# 如果你能理解上面一段,就不用看下面的廢話了。

下面是詳細的回答:

說真的,這簡直是我最近在知乎遇到過的最好的問題!

這個問題遠超我想像中的複雜。我本來以為我能用兩分鐘搞定這種每日一水的問題,結果我花了一個小時搜來搜去,讀來讀去,還跑去群里跟人討論了一陣,都沒能找到答案。

大概兩個小時以後,我找到了相對正確的答案,把自己已經弄懂的部分強答一番,並邀請一些大神,希望能看到更為準確的回答。

這個問題的博大精深在於,能從中扯出許多小問題來,雖然這些東西很細枝末節,很trick,在日常編程中不怎麼用的到,更不怎麼需要額外關注,但是理解這些問題,對於我們理解Python的對象機制乃至內存處理機制有很大的幫助。

我從頭開始說,大概會分以下幾個部分來談,每個部分其實都能展開很廣,這次就把與問題相關的知識簡單一提:

(雖然我覺得按照我尋找答案的過程講,可能對認知更有幫助,但是理清頭緒的話可能更好理解,之後會找時間為這個問題寫篇文章好好記錄一下)

  1. Python中的數據類型——可變與不可變
  2. Python中is比較與==比較的區別
  3. Python中對小整數的緩存機制
  4. Python程序的結構——代碼塊
  5. Python的內存管理——新建對象時的操作

聲明:以下所講機制,與Python不同版本的具體實現有關(implement specific)可能不同。

Python中的數據類型

Python中的數據類型,這可能是大家入門Python的第一節課。很簡單嘛,大家最常用的,int(包括long)、float、string、list、tuple、dict,加上bool和NoneType。

但是這裡要重點說的,其實是可變類型和不可變類型。

不可變(immutable):Number(包括int、float),String,Tuple

可變(mutable):Dict,List,User-defined class

首先我們要記住一句話,一切皆對象。Python中把任何一種Type都當作對象來處理。其中有一些類型是不可變的,比如:

這個還是好理解的,在初始化賦值一個字元串後,我們沒有辦法直接修改它的值。但是數字呢?數字這種變來變去的又怎麼理解。

可以看出,a的值雖然從10變成了11,但是a這個變數指向內存中的位置發生了變化,也就是說我們並沒有對a指向的內存進行操作,而是對a進行了重新賦值。

再簡單舉一個可變的例子。

體會了可變與不可變的外在表現後,簡單理解一下為什麼不可變。

Python官方文檔這樣解釋字元串不可變:

There are several advantages.

One is performance: knowing that a string is immutable means we can allocate space for it at creation time, and the storage requirements are fixed and unchanging. This is also one of the reasons for the distinction between tuples and lists.

Another advantage is that strings in Python are considered as 「elemental」 as numbers. No amount of activity will change the value 8 to anything else, and in Python, no amount of activity will change the string 「eight」 to anything else.

個人感覺,有性能上的考慮(比如對一些固定不變的元素給予固定的存儲位置,整數這樣操作比較方便,字元串的話涉及一些比較也會減少後續操作的時間),也有一些安全上的考慮(比如列表中的值會改變,元組不會)。這個我也不太精通,就不展開談了。

Python中is比較與==比較的區別

前面已經提過一次,Python中一切皆對象。對象包含三個要素,id、type、value。

而Python中用於比較「相等」這一概念的操作符,is和==。

當兩個變數指向了同一個對象時,is會返回True(即is比較的是兩個變數的id);

當兩個變數的值相同時,==會返回True(即==比較的是兩個變數的value)。

示例(命令行交互模式下):

第一個和第三個示例是好理解的。

但是第二個就不那麼好理解了,尤其是配合下面這個(假定我們已經知道命令行中的語句執行是單獨執行兩次不會相互影響,後面會具體解釋):

為什麼a、b分別賦值1000時is比較返回False,可以分別賦值100就會返回True?

Python中對小整數的緩存機制

Python官方文檔中這麼說:

The current implementation keeps an array of integer objects for all integers between -5 and 256, when you create an int in that range you actually just get back a reference to the existing object. So it should be possible to change the value of 1. I suspect the behaviour of Python in this case is undefined. :-)

簡單來說就是,Python自動將-5~256的整數進行了緩存,當你將這些整數賦值給變數時,並不會重新創建對象,而是使用已經創建好的緩存對象。

Python程序的結構——代碼塊Python的內存管理——新建對象時的操作

終於要來到題主問題的部分了。

先來看最讓我們困惑的,也就是題主給出的示例吧(接下來用float演示,int是同樣的情況):

交互命令行下:

同樣的還有:

(說好的小整數才有緩存呢(摔)!這跟你講的不一樣啊教練!)

這就很尷尬了對吧。

其實從結果論出發,我們很容易猜到結論,就像題主自己也猜了個差不多——緩存機制不同。畢竟is比較的就是對象的id,也就是對象在內存中的位置,也就是是不是同一個對象。

既然腳本文件的執行結果是True,那麼,他倆就是同一個對象;既然命令行執行的結果是False,那麼他倆就不是同一個對象。(這他喵的不是廢話嗎!

所以我開始了漫長的找原理的過程……然而網上這方面提及的實在太少。尤其是大家的大部分討論都是int的小整數緩存機制;就算討論到了float,也不實際解決我們的問題。(比如有小夥伴幫我找到的鏈接https://groups.google.com/forum/m/#!topic/comp.lang.python/EsLWI3Mogig)

其實我都快要放棄了,漫無目的地翻stackoverflow推薦的相關問題時終於找到了一個類似的情況,但是人家並不是比較的腳本文件和命令行執行,而是比較的函數體和賦值語句:

同樣的代碼,拆開就是False,放函數里就是True!是不是很像我們遇到的情況了。

根據提示我們從官方文檔找到了這樣的說法:

A Python program is constructed from code blocks. A block is a piece of Python program text that is executed as a unit. The following are blocks: a module, a function body, and a class definition. Each command typed interactively is a block. A script file (a file given as standard input to the interpreter or specified as a command line argument to the interpreter) is a code block. A script command (a command specified on the interpreter command line with the 『-c『 option) is a code block. The string argument passed to the built-in functions eval() and exec() is a code block.

A code block is executed in an execution frame. A frame contains some administrative information (used for debugging) and determines where and how execution continues after the code block』s execution has completed.

沒錯!跟我們猜的一樣!這就是原理的出處了!

代碼塊作為一個執行單元,一個模塊、一個函數體、一個類定義、一個腳本文件,都是一個代碼塊。

在互動式命令行中,每行代碼單獨視作一個代碼塊。

至此問題解決……了嗎?視作一個代碼塊,就意味著要把相同value的賦值指向相同的對象嗎?

在此重複一下"is" operator behaves unexpectedly with non-cached integers中提到的實驗,並簡單翻譯結論。

通過compile()函數和dis模塊的code_info()函數來檢測我們執行的命令的信息。

示例:

可以看出,分別賦值a,b得到的value相等,id是不一樣的。

把10.0 10.0 10.1分別賦值給a,b,c,可以看出結果中其實只保存了一個10.0,也就是a,b共用了這個數值。

也就是說,當命令行執行時,是以single的模式來compile代碼(2. Built-in Functions)。它會在u_consts字典中記錄對象常量。

The mode argument specifies what kind of code must be compiled; it can be "exec" if source consists of a sequence of statements, "eval" if it consists of a single expression, or "single" if it consists of a single interactive statement (in the latter case, expression statements that evaluate to something other than None will be printed).

而在同一代碼塊執行時,當增加新的常量,會先在字典中查詢記錄,所以相同賦值的變數會指向同一個對象而不是新建對象。

至此…問題大概是解決了。

(當然這是implement specific的事情,參見https://docs.python.org/3/reference/datamodel.html#objects-values-and-types中的CPython implementation detail

要說這道題教會了我什麼……

那一定是:比較數值請一定一定用==不要用is!

以上。

參考資料:

Immutable vs Mutable types

Design and History FAQ

"is" operator behaves unexpectedly with integers

Integer Objects - Python 3.5.2 documentation

"is" operator behaves unexpectedly with non-cached integers

"is" operator behaves unexpectedly with floats

What is the difference between "a is b" and "id(a) == id(b)" in Python?

Same value for id(float)

Python Memory Management


以前就有過基本上同樣的問題,這是我以前的回答的傳送門:python idle 解釋和直接 python script.py 解釋有什麼差別? - RednaxelaFX的回答 - 知乎


高票 @段小草 已經回答的很詳細了。我在他的回答之上,再補充一點東西,同時給大家介紹一個查看文件結構的工具,這樣如果再遇到不理解的問題,可以直接通過分析pyc文件自己找到答案。然後,我又把這個文章放到我的專欄里了:

詳解python中的 is 操作符

is 操作符是Python語言的一個內建的操作符。它的作用在於比較兩個變數是否指向了同一個對象。

與 == 的區別

class A():
def __init__(self, v):
self.value = v

def __eq__(self, t):
return self.value == t.value

a = A(3)
b = A(3)

print a == b
print a is b

這個結果是True,False。因為我們重寫了__eq__方法就使得a, b在比較的時候,只比較它們的value即可。只要它們的value相等,那麼a, b就是相等的。

而 is 操作符是判斷兩個變數是否引用了同一個對象。

同一個對象?

is 的用法說起來其實挺簡單的,但是真正用起來,它的難點恰恰就在於判斷哪些對象是同一個對象。

看下面的幾個測試,先不看結果,自己能答對多少?

a = 10
b = 10
print a is b

a = 10.0
b = 10.0
print a is b

a = 10
def f():
return 10

print f() is a

a = 1000
def f():
return 1000

print f() is a

a = 10.0
def f():
return 10.0
print f() is a

嗯。這個結果是True, True, True, False, False。你答對了嗎?

這個結果中牽扯到兩個問題:第一,就是小整數的緩存,第二,就是pyc文件中CodeObject的組織問題。

Python中把-127到128這些小整數都緩存了一份。這和Java的Integer類是一樣的。所以,對於-127到128之間的整數,整個Python虛擬機中就只有一個實例。不管你什麼時候,什麼場景下去使用 is 進行判斷,都會是True,所以我們知道了這兩個測試一定會是True:

a = 10
b = 10
print a is b

a = 10
def f():
return 10

print f() is a

接著,我們重點看下,這兩個測試:

a = 10.0
b = 10.0
print a is b

a = 10.0
def f():
return 10.0
print f() is a

為什麼一個是True,一個是False。要探究這個問題,就要從位元組碼的角度去分析了。我們先把這個文件編譯一下:

python -m compileall testis.py

然後再使用這個工具查看一下位元組碼文件:https://github.com/hinus/railgun/blob/master/src/main/python/rgparser/show.py

得到這樣的輸出:

&
& 0 &
& 0&
& 2&
& 0040&
&
6400005a00006400005a01006500006501006b080047486400005a000064
01008400005a02006502008300006500006b0800474864020053
&

&
1 0 LOAD_CONST 0 (10.0)
3 STORE_NAME 0 (a)

2 6 LOAD_CONST 0 (10.0)
9 STORE_NAME 1 (b)

3 12 LOAD_NAME 0 (a)
15 LOAD_NAME 1 (b)
18 COMPARE_OP 8 (is)
21 PRINT_ITEM
22 PRINT_NEWLINE

5 23 LOAD_CONST 0 (10.0)
26 STORE_NAME 0 (a)

6 29 LOAD_CONST 1 (&)
32 MAKE_FUNCTION 0
35 STORE_NAME 2 (f)

8 38 LOAD_NAME 2 (f)
41 CALL_FUNCTION 0
44 LOAD_NAME 0 (a)
47 COMPARE_OP 8 (is)
50 PRINT_ITEM
51 PRINT_NEWLINE
52 LOAD_CONST 2 (None)
55 RETURN_VALUE
&
& ("a", "b", "f")&
& ()&
& ()&
& ()&
& "testis.py"&
& "&"&
& 1&
&
10.0
&
& 0 &
& 0&
& 1&
& 0043&
& 64010053&
&
7 0 LOAD_CONST 1 (10.0)
3 RETURN_VALUE
&

& ()&
& ()&
& ()&
& ()&
& "testis.py"&
& "f"&
& 6&
&
None
10.0
&

& 0001&
&

None
&

& 060106010b0206010902&
&

大家注意看,整個python文件其實就是一個大的&對象,f 所對應的那個函數也是一個&對象,這個code對象做為整體是大的&對象的consts域里的一個const項。再注意,在大&對象里,有10.0這樣的一個const項,f 這個&對象所對應的conts里呢,也有一個10.0這個浮點數。

當python在載入這個文件的時候,就會完成主&里的10.0這個浮點數的載入,生成一個PyFloatObject。也就是說靜態的pyc文件的常量表在被載入以後,就變成了內存中的常量表,文件的表裡的10.0就變成了內存中的一個PyFloatObject。所以,a, b兩個變數都會引用這個PyFloatObject。

但是 f 里的那個10.0呢?它是要等到MAKE_FUNCTION被調用的時候才會真正地初始化。做為 f 方法的返回值,它必然與我們之前所說的主&里的10.0不是同一個對象了。

本質上講,這是Python的一個設計缺陷(例如Java以一個文件為編譯單元,共享同一個常量池就會減輕這個問題。但如果跨文件使用 == 操作符,也會出現同樣的問題。仍然沒有解決這個問題。實際上,我自己也不知道該怎麼解決這個問題。)我們應該盡量避免 is 的這種用法。始終把 is 的用法限制在本文的第一個例子中。這樣相對會安全一些。

更多編程相關的問題,請關注我的公眾號:

我的公眾號


如果你知道c++編譯器下有兩個概念叫常量摺疊和複製傳播的話,相信你會更好理解這個問題。


因為放在文件中時,整個文件作為module是整體編譯的,編譯過程中會對一些常量做合併,所以兩個10.0會合併為一個對象放在編譯結果的常量區,而交互模式下,每一行命令(嚴格說是每一個輸入結束的命令,比如你輸入一個while循環,是等循環體輸入完成後才算一個命令,又或者連續多條在單行的命令)是單獨編譯然後立即執行的,因此這裡a和b的賦值分別是引用了兩個不同float對象,如果你這麼寫:

10.0 is 10.0

或者

a = 10.0; b = 10.0; a is b

結果就是True了,因為是作為整體編譯的

有人會問py不是解釋型的么,為啥要說編譯,實際上是編譯為位元組碼再解釋執行的,形式上和java的編譯到class再執行沒啥不同,只是給你批處理化了,不用手動那麼麻煩

另外,py中的is,id等和對象地址相關的東西和實現細節有關,有時候會讓你摸不著頭腦,比如說,交互模式下:

a = "hello"

b = "hello"

print a is b

結果是True,如上所述,如果這三句寫到文件那也是True

然而你把上面字元串換成"hello world"再在交互和文件中試試?

再比如,id可以拿到一個對象的內存地址,但是看這個例子:

&>&>&> str(12) is str(34)

False

&>&>&> id(str(12)) == id(str(34))

True

上面的False是顯然的,不同的兩個字元串,然而下面的結果為啥捏,呵呵……

------------------------------

很久以前在群里講課,講過這個問題:

python對象的引用-python2群20130223群內授課記錄


我覺得,如果我開一個新問題【為什麼Python的第一個字母要大寫?】,估計也會有答主洋洋洒洒寫個幾萬字來解釋清楚。


簡單的來說呢,直接以文件的形式運行,編譯生成的pyc文件中只會有一個10.0的float的對象,運行時產量a和產量b都指向相同的此對象,當然a is b返回True。同理在命令行里執行 a, b=10.0 10.0 執行a is b也是返回True,倘若分開賦值,則會創建兩個float對象,a is b 就返回False。如果想要更深入的了解python,讀一讀python源碼分析,再看看源代碼,自然而然就知道了。


老生長談了。我一個搞cpp的都知道java .net py這種語言的類型都是引用類型。虛擬機會開一個對象池儲存一些整數。a=10,b=10的時候,a b指向池裡面的同一個10。他們就是一個東西。

如果虛擬機沒有開對象池,a是一個10,b是另一個10,當然不一樣。

你用==比較的話,虛擬機會給你拆箱,去比較數值,而不是指針。

如果用c語言的觀點看。

第一種就是

int num=10;

int *a=num,int*b=num;

assert(a==b) true

第二種就是

int *a=new int(10)

int *b=new int(10)

assert(a==b) false


Python中一切都是對象,同樣是10.0 申請的內存地址不一樣


推薦閱讀:

在Mac系統下python如何安裝第三方函數庫?
Python3如何實現兩個列表的交叉列印?
初學python,pycharm和Spyder哪個好?
python3.5有哪些可用的第三方模塊?
如果只推介一本python3的書籍,你會推介哪一本?

TAG:Python | 編程 | Python3x | PyCharm | Python開發 |