Python 中的 is 和 id

pythontab發表於2014-01-03

(ob1 is ob2) 等價於 (id(ob1) == id(ob2))

  首先id函式可以獲得物件的記憶體地址,如果兩個物件的記憶體地址是一樣的,那麼這兩個物件肯定是一個物件。和is是等價的。Python原始碼為證。

static PyObject *
 cmp_outcome(int op, register PyObject *v, register PyObject *w)
{
 int res = 0;
 switch (op) {
 case PyCmp_IS:
  res = (v == w);
 break;
 case PyCmp_IS_NOT:
res = (v != w);
 break;

  但是請看下邊程式碼的這種情況怎麼會出現呢?

In [1]: def bar(self, x):

...:     return self.x + y

...: 

In [2]: class Foo(object):

...:     x = 9

...:     def __init__(self ,x):

...:         self.x = x

...:     bar = bar

...:     

In [3]: foo = Foo(5)

In [4]: foo.bar is Foo.bar

Out[4]: False

In [5]: id(foo.bar) == id(Foo.bar)

Out[5]: True

  兩個物件用is判斷是False,用id判斷卻是True,這與我們已知的事實不符啊,這種現象該如何解釋呢?遇到這種情況最好的解決方法就是呼叫dis模組去看下兩個比較語句到底做了什麼。

In [7]: dis.dis("id(foo.bar) == id(Foo.bar)")

          0 BUILD_MAP       10340

          3 BUILD_TUPLE     28527

          6 <46>           

          7 DELETE_GLOBAL   29281 (29281)

         10 STORE_SLICE+1 

         11 SLICE+2       

         12 DELETE_SUBSCR  

         13 DELETE_SUBSCR  

         14 SLICE+2       

         15 BUILD_MAP       10340

         18 PRINT_EXPR     

         19 JUMP_IF_FALSE_OR_POP 11887

         22 DELETE_GLOBAL   29281 (29281)

         25 STORE_SLICE+1 

 

In [8]: dis.dis("foo.bar is Foo.bar")

          0 BUILD_TUPLE     28527

          3 <46>           

          4 DELETE_GLOBAL   29281 (29281)

          7 SLICE+2       

          8 BUILD_MAP        8307

         11 PRINT_EXPR     

         12 JUMP_IF_FALSE_OR_POP 11887

         15 DELETE_GLOBAL   29281 (29281)

  真實情況是當執行.運算子的時候,實際是生成了一個proxy物件,foo.bar is Foo.bar的時候,兩個物件順序生成,放在棧裡相比較,由於地址不同肯定是False,但是id(foo.bar) == id(Foo.bar)的時候就不同了,首先生成foo.bar,然後計算foo.bar的地址,計算完之後foo.bar的地址之後,就沒有任何物件指向foo.bar了,所以foo.bar物件就會被釋放。然後生成Foo.bar物件,由於foo.bar和Foo.bar所佔用的記憶體大小是一樣的,所以又恰好重用了原先foo.bar的記憶體地址,所以id(foo.bar) == id(Foo.bar)的結果是True。

  下面內容由郵件Leo Jay大牛提供,他解釋的更加通透。

  用id(expression a) == id(expression b)來判斷兩個表示式的結果是不是同一個物件的想法是有問題的。

  foo.bar 這種形式叫 attribute reference [1],它是表示式的一種。foo是一個instance object,bar是一個方法,這個時候表示式foo.bar返回的結果叫method object [2]。根據文件:

When an instance attribute is referenced that isn’t a data attribute, 

its class is searched. If the name denotes a valid class attribute 

that is a function object, a method object is created by packing 

(pointers to) the instance object and the function object just found 

together in an abstract object: this is the method object.

  foo.bar本身並不是簡單的名字,而是表示式的計算結果,是一個 method object,在id(foo.bar)這樣的表示式裡,method object只是一個臨時的中間變數而已,對臨時的中間變數做id是沒有意義的。

  一個更明顯的例子是,

print id(foo.bar) == id(foo.__init__)

  輸出的結果也是True

  看 id 的文件[3]:

Return the “identity” of an object. This is an integer (or long 

integer) which is guaranteed to be unique and constant for this object 

during its lifetime. Two objects with non-overlapping lifetimes may 

have the same id() value. 

CPython implementation detail: This is the address of the object in memory.

  只有你能保證物件不會被銷燬的前提下,你才能用 id 來比較兩個物件。所以,如果你非要比的話,得這樣寫:

fb = foo.bar 

Fb = Foo.bar 

print id(fb) == id(Fb)

  即把兩個表示式的結果繫結到名字上,再來比是不是同一個物件,你才能得到正確的結果。

  is表示式 [4] 也是一樣的,你現在得到了正確的結果,完全是因為 CPython 現在的實現細節決定的。現在的is的實現,是左右兩邊的物件都計算出來,然後再比較這兩個物件的地址是否一樣。萬一哪天改成了,先算左邊,儲存地址,把左邊釋放掉,再算右邊,再比較的話,你的is的結果可能就錯了。官方文件裡也提到了這個問題 [5]。我認為正確的方法也是像id那樣,先把左右兩邊都計算下來,並顯式繫結到各自的名字上,然後再用is判斷。


相關文章