2016-06-11 23 views
4

例如:對原始類型變量執行「=」時,CPython實際上會做什麼?

a = some_process_that_generates_integer_result() 
b = a 

有人告訴我,b一個將指向整數對象的同一塊,從而b將修改該對象的引用計數。該代碼在功能PyObject* ast2obj_expr(void* _o)Python的ast.c執行:

static PyObject* ast2obj_object(void *o) 
{ 
    if (!o) 
     o = Py_None; 
    Py_INCREF((PyObject*)o); 
    return (PyObject*)o; 
} 

...... 

case Num_kind: 
    result = PyType_GenericNew(Num_type, NULL, NULL); 
    if (!result) goto failed; 
    value = ast2obj_object(o->v.Num.n); 
    if (!value) goto failed; 
    if (PyObject_SetAttrString(result, "n", value) == -1) 
      goto failed; 
    Py_DECREF(value); 
    break; 

不過,我認爲修改引用計數沒有所有權變更確實是徒勞的。我期望的是每個包含原始值(浮點數,整數等)的變量總是有自己的值,而不是指向同一個對象。

而且在我簡單的測試代碼的執行,我發現上面的Num_kind分支破發點從未達到:

def some_function(x, y): 
    return (x+y)*(x-y) 

a = some_function(666666,66666) 
print a 

b = a 
print a 
print b 

b = a + 999999 
print a 
print b 

b = a 
print a 
print b 

我使用的是由Debian提供的python2.7-DBG程序。我確定程序和源代碼是相匹配的,因爲許多其他斷點正常工作。

那麼,CPython實際上對原始類型對象做了什麼?

+0

我不確定你在這裏問什麼。你指的是什麼斷點?您希望通過該代碼得到什麼樣的行爲?它是如何不符合您的期望的? – poke

+0

@poke添加缺少的描述。斷點位於上面C代碼的Num_kind分支中,正如有人告訴我那段代碼與運行時數字變量複製相關。 – jiandingzhe

+1

「每個變量保持原始值」。 Python實際上並不擁有持有值的變量。相反,它具有可以綁定到名稱的強類型對象。這與C風格的數據模型完全不同,嘗試以C術語理解Python數據模型很少有成果,除非您碰巧與C代碼交互。您可能會發現這篇文章有用:[關於Python名稱和值的事實和神話](http://nedbatchelder.com/text/names.html),這是由SO老將Ned Batchelder編寫的。 –

回答

7

首先,Python中沒有「原始對象」。一切都是同一種客體,在語言層面上它們都以相同的方式處理。這樣,下面的分配相同的方式工作,無論它們被分配的值的:

a = some_process_that_generates_integer_result() 
b = a 

在Python,分配是總是參考副本。所以無論函數返回什麼,它的參考是複製到變量a。然後在第二行中,參考再次被複製到變量b中。因此,這兩個變量都會引用完全相同的對象。

您可以輕鬆地通過使用id()功能,會告訴你一個對象的身份驗證這一點:

print id(a) 
print id(b) 

這將打印相同識別數的兩倍。但請注意,這樣做會複製參考兩次:它不是傳遞函數的變量,而是引用的副本。

這是從那裏你常常之間區分「呼叫按值」「呼叫參考」其他語言不同。前者表示您創建該值的副本並將其傳遞給函數,這意味着將爲該值分配新的內存;後者意味着實際引用被傳遞,並且對該引用的更改也會影響原始變量。

Python在通常被稱爲「通過呼叫分配」:每個函數調用,你傳遞參數實質上是一個分配到新的變量(隨後可到功能)。並且一個作業複製參考。

當一切都是對象時,這實際上是一個非常簡單的策略。正如我上面所說的,整數發生的事情與其他對象發生的事情沒有什麼不同。關於整數的唯一「特殊」事情是它們是不變的,所以你不能改變它們的值。這意味着整數對象總是指與值完全相同的。這可以很容易地與多個值共享對象(在內存中)。每一個產生新結果的操作都會給你一個不同的對象,所以當你進行一系列算術運算時,你實際上正在改變一個變量指向的對象。

其他不可變對象也是如此,例如字符串。每個產生更改字符串的操作都會爲您提供不同的字符串對象。

但是可變對象的賦值也是一樣的。只是改變這些對象的價值是可能的,所以它們看起來不同。考慮下面這個例子:

a = [1] # creates a new list object 
b = a # copies the reference to that same list object 
c = [2] # creates a new list object 
b = a + C# concats the two lists and creates a new list object 
d = b 
# at this point, we have *three* list objects 
d.append(3) # mutates the list object 
print(d) 
print(b) # same result since b and d reference the same list object 

現在回到你的問題,你舉那裏的C代碼,你實際上是在尋找CPython中的錯誤部分得到解釋那裏。 AST是解析器在解析文件時創建的抽象語法樹。它反映了程序的語法結構,但沒有提到實際的運行時行爲。

您爲Num_kind顯示的代碼實際上負責創建Num AST對象。您可以使用ast module時得到這樣一個想法:

>>> import ast 
>>> doc = ast.parse('foo = 5') 

# the document contains an assignment 
>>> doc.body[0] 
<_ast.Assign object at 0x0000000002322278> 

# the target of that assignment has the id `foo` 
>>> doc.body[0].targets[0].id 
'foo' 

# and the value of that assignment is the `Num` object that was 
# created in that C code, with that `n` property containing the value 
>>> doc.body[0].value 
<_ast.Num object at 0x00000000023224E0> 
>>> doc.body[0].value.n 
5 

如果你想獲得的Python代碼的實際評價的想法,你應該先看看字節碼。字節碼是虛擬機在運行時執行的內容。您可以使用dis module看到字節代碼Python代碼:

>>> def test(): 
     foo = 5 

>>> import dis 
>>> dis.dis(test) 
    2   0 LOAD_CONST    1 (5) 
       3 STORE_FAST    0 (foo) 
       6 LOAD_CONST    0 (None) 
       9 RETURN_VALUE 

正如你所看到的,主要有兩個字節碼指令這裏:LOAD_CONSTSTORE_FASTLOAD_CONST只會將一個常數值加載到評估堆棧上。在這個例子中,我們只是加載一個常數,但我們也可以從函數調用中加載值(嘗試使用dis模塊來弄清楚它是如何工作的)。

作業本身使用STORE_FAST。字節碼解釋器確實the following該指令:

TARGET(STORE_FAST) 
{ 
    v = POP(); 
    SETLOCAL(oparg, v); 
    FAST_DISPATCH(); 
} 

所以它本質上會從堆棧中的值(參考整數對象),然後調用SETLOCAL基本上只會賦值給局部變量。

但請注意,這並不會增加該值的引用計數。這與LOAD_CONST會發生什麼,或任何其他字節碼指令從什麼地方得到的值:

TARGET(LOAD_CONST) 
{ 
    x = GETITEM(consts, oparg); 
    Py_INCREF(x); 
    PUSH(x); 
    FAST_DISPATCH(); 
} 

所以TL;博士:在Python分配總是參考副本。每當使用一個值時,也會複製引用(但在複製引用僅在很短時間內存在的許多其他情況下)。 AST負責創建解析程序的對象表示(僅限語法),而字節代碼解釋器運行先前編譯的字節代碼以在運行時執行實際的東西並處理真實的對象。

相關問題