i = 1
? 這是一個再簡單不過的賦值語句,即便是才開始學(xué)習(xí)編程的新手也能脫口而出它的含義 -- “設(shè)置變量i的值為1”。
i = 2
? “將變量i的值改為2”,當(dāng)看到接下來這行代碼時,你腦海中肯定會立即浮現(xiàn)這樣的念頭。
? 這難道會有問題嘛?這簡簡單單的一行賦值語句其實包含了python中的三個重要概念:名字、綁定和對象。
python對賦值語句作出了自己的定義:
? “符值語句是用來將名字綁定(或重新綁定)到某個對象的操作,而且它也可用來修改可變對象的屬性或
對象中所包含的成員。”
? 名字綁定到對象這個概念在python中隨處可見,可以說是python的最基本而且最重要的概念之一。如果
沒有很好理解這一點,一些意想不到的結(jié)果就會在您的代碼中悄然出現(xiàn)。
? 先來看一個簡單例子:
>>> a = {'g':1}
>>> b = a*4
>>> print b
[{'g': 1}, {'g': 1}, {'g': 1}, {'g': 1}]
>>> b[0]['g'] = 2
>>> print b
? 出乎意料嘛?請慢慢看完這篇文章。
1. 對象
? “萬物皆對象”(Everything is object),這是python這種面向?qū)ο笳Z言所倡導(dǎo)的理念。在我們熟悉的C++中,1只是一個整型數(shù),而不是一個對象。但在python中,1卻是一個實實在在的對象,您可以用dir(1)來顯示它的屬性。
? 在python中,所有對象都有下面三個特征:
?* 唯一的標(biāo)識碼(identity)
?* 類型
?* 內(nèi)容(或稱為值)
? 一旦對象被創(chuàng)建,它的標(biāo)識碼就不允許更改。對象的標(biāo)識碼可以有內(nèi)建函數(shù)id()獲取,它是一個整型數(shù)。您可以將它想象為該對象在內(nèi)存中的地址,其實在目前的實現(xiàn)中標(biāo)識碼也就是該對象的內(nèi)存地址。
>>> class c1:
?pass
...
>>> obj = c1()
>>> obj
<__main__.c1 instance at 0x00AC0738>
>>> id(obj)
11274040
? 換算一下,11274040就是十六進(jìn)制的0x00AC0738。
>>> id(1)
7957136
? 這就是前面提到的1這個對象的標(biāo)識碼,也就是它在內(nèi)存中的地址。
? 當(dāng)用is操作符比較兩個對象時,就是在比較它們的標(biāo)識碼。更確切地說,is操作符是在判斷兩個對象是否是同一個對象。
>>> [1] is [1]
? 其結(jié)果是False,是因為這是兩個不同的對象,存儲在內(nèi)存中的不同地方。
>>> [1] == [1]
? 其結(jié)果是True,是因為這兩個不同的對象有著相同的值。
? 與對象的標(biāo)識碼類似,對象的類型也是不可更改的。可以用內(nèi)建函數(shù)type()取得對象的類型。
? 有的對象的值是可以改變的,這類對象叫作可變對象;而另外一些對象在創(chuàng)建后其值是不可改變的(如1這個對象),這類對象叫作恒定對象。對象的可變性是由它的類型決定的,比如數(shù)值型(number)、字符串型(string)以及序列型(tuple)的對象是恒定對象;而字典型(dictionary)和列表型(list)的對象是可變對象。
? 除了上面提到的三個特征外,一個對象可能:
?* 沒有或者擁有多個方法
?* 沒有或者有多個名字
2. 名字
? 名字是對一個對象的稱呼,一個對象可以只有一個名字,也可以沒有名字或取多個名字。但對象自己卻不知道有多少名字,叫什么,只有名字本身知道它所指向的是個什么對象。給對象取一個名字的操作叫作命名,python將賦值語句認(rèn)為是一個命名操作(或者稱為名字綁定)。
? 名字在一定的名字空間內(nèi)有效,而且唯一,不可能在同一個名字空間內(nèi)有兩個或更多的對象取同一名字。
? 讓我們再來看看本篇的第一個例子:i = 1。在python中,它有如下兩個含義:
?* 創(chuàng)建一個值為1的整型對象
?* "i"是指向該整型對象的名字(而且它是一個引用)
?
3. 綁定
? 如上所講的,綁定就是將一個對象與一個名字聯(lián)系起來。更確切地講,就是增加該對象的引用計數(shù)。眾所周知,C++中一大問題就是內(nèi)存泄漏 -- 即動態(tài)分配的內(nèi)存沒有能夠回收,而解決這一問題的利器之一就是引用計數(shù)。python就采用了這一技術(shù)實現(xiàn)其垃圾回收機制。
?
? python中的所有對象都有引用計數(shù)。
i=i+1
* 這創(chuàng)建了一個新的對象,其值為i+1。
* "i"這個名字指向了該新建的對象,該對象的引用計數(shù)加一,而"i"以前所指向的老對象的
? 引用計數(shù)減一。
* "i"所指向的老對象的值并沒有改變。
* 這就是為什么在python中沒有++、--這樣的單目運算符的一個原因。
3.1 引用計數(shù)
? 對象的引用計數(shù)在下列情況下會增加:
?* 賦值操作
?* 在一個容器(列表,序列,字典等等)中包含該對象
? 對象的引用計數(shù)在下列情況下會減少:
?* 離開了當(dāng)前的名字空間(該名字空間中的本地名字都會被銷毀)
?* 對象的一個名字被綁定到另外一個對象
?* 對象從包含它的容器中移除
?* 名字被顯示地用del銷毀(如:del i)
? 當(dāng)對象的引用計數(shù)降到0后,該對象就會被銷毀,其所占的內(nèi)存也就得以回收。
4. 名字綁定所帶來的一些奇特現(xiàn)象
例4.1:
>>> li1 = [7, 8, 9, 10]
>>> li2 = li1
>>> li1[1] = 16
>>> print li2
[7, 16, 9, 10]
注解:這里li1與li2都指向同一個列表對象[7, 8, 9, 10],“l(fā)i[1] = 16”是改變該列表中的第2個元素,所以通過li2時同樣會看到這一改動。
例4.2:
>>> b = [{'g':1}]*4
>>> print b
[{'g': 1}, {'g': 1}, {'g': 1}, {'g': 1}]
>>> b[0]['g'] = 2
>>> print b
[{'g': 2}, {'g': 2}, {'g': 2}, {'g': 2}]
例4.3:
>>> b = [{'g':1}] + [{'g':1}] + [{'g':1}] + [{'g':1}]
>>> print b
[{'g': 1}, {'g': 1}, {'g': 1}, {'g': 1}]
>>> b[0]['g'] = 2
>>> print b
[{'g': 2}, {'g': 1}, {'g': 1}, {'g': 1}]
注解:在有的python書中講到乘法符號(*)就相當(dāng)于幾個加法的重復(fù),即認(rèn)為例4.2應(yīng)該與4.3的結(jié)果一致。
????? 其實不然。例4.2中的b這個列表中的每一個元素{'g': 1}其實都是同一個對象,可以用id(b[n])進(jìn)行驗證。而例4.3中則是四個不同的對象。我們可以采用名字綁定的方法消除這一歧義:
>>> a = {'g' : 1}
>>> b = [a]*4
>>> b[0]['g'] = 2
>>> print b
[{'g': 2}, {'g': 2}, {'g': 2}, {'g': 2}]
>>> print a
{'g': 2}
>>> a = {'g' : 1}
>>> b = [a] + [a] + [a] + [a]
>>> b[0]['g'] = 2
>>> print b
[{'g': 2}, {'g': 2}, {'g': 2}, {'g': 2}]
>>> print a
{'g': 2}
? 不過對于恒定對象而言,“*”和連續(xù)加法的效果一樣。比如,b=[1] * 4 就等同于 b=[1]+[1]+[1]+[1]。
5. 函數(shù)的傳參問題
? 函數(shù)的參數(shù)傳遞也是一個名字與對象的綁定過程,而且是綁定到另外一個名字空間(即函數(shù)體內(nèi)部的名字空間)。python對賦值語句的獨特看法又會對函數(shù)的傳遞造成什么影響呢?
5.1 傳值?傳址?
? 在學(xué)習(xí)C++的時候我們都知道有兩種參數(shù)傳遞方式:傳值和傳址。而在python中所有的參數(shù)傳遞都是引用傳遞(pass reference),也就是傳址。這是由于名字是對象的一個引用這一python的特性而自然得來的,在函數(shù)體內(nèi)部對某一外部可變對象作了修改肯定會將其改變帶到函數(shù)以外。讓我們來看看下面
這個例子:
例5.1
>>> a = [1, 2, 3]
>>> def foo(par):
...?par[1] = 10
...
>>> foo(a)
>>> print a
[1, 10, 3]
? 因此,在python中,我們應(yīng)該拋開傳遞參數(shù)這種概念,時刻牢記函數(shù)的調(diào)用參數(shù)是將對象用另外一個名字空間的名字綁定。在函數(shù)中,不過是用了另外一個名字,但還是對這同一個對象進(jìn)行操作。
5.2 缺省參數(shù)
? 使用缺省參數(shù),是我們喜愛的一種作法。這可以在調(diào)用該函數(shù)時節(jié)省不少的擊鍵次數(shù),而且代碼也顯得更加簡潔。更重要的是它從某種意義上體現(xiàn)了這個函數(shù)設(shè)計的初衷。
? 但是python中的缺省參數(shù),卻隱藏著一個玄機,初學(xué)者肯定會在上面栽跟頭,而且這個錯誤非常隱秘。先看看下面這個例子:
例5.2
>>> def foo(par=[]):
...?par.append(0)
...?print par
...?
>>> foo()?????????????????????? # 第一次調(diào)用
[0]
>>> foo()?????????????????????? # 第二次調(diào)用
[0, 0]
? 出了什么問題?這個參數(shù)par好像類似與C中的靜態(tài)變量,累計了以前的結(jié)果。是這樣嗎?當(dāng)然不是,這都是“對象、名字、綁定”這些思想惹的“禍”?!叭f物皆對象”,還記得嗎?這里,函數(shù)foo當(dāng)然也是一個對象,可以稱之為函數(shù)對象(與一般的對象沒什么不同)。先來看看這個對象有些什么屬性。
>>> dir(foo)
['__call__', '__class__', '__delattr__', '__dict__', '__doc__', '__get__', '__getattribute__', '__hash__', '__init__', '__module__', '__name__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__str__', 'func_closure', 'func_code', 'func_defaults', 'func_dict', 'func_doc', 'func_globals', 'func_name']
? 單從名字上看,“func_defaults”很可能與缺省參數(shù)有關(guān),看看它的值。
>>> foo.func_defaults????????? # 顯示這個屬性的內(nèi)容
([0, 0],)
>>> foo()????????????????????? # 第三次調(diào)用
[0, 0, 0]
>>> foo.func_defaults????????? # 再來看看這個屬性
([0, 0, 0],)
? 果不其然,就是這個序列對象(tuple)包含了所有的缺省參數(shù)。驗證一下:
>>> def fooM(par1, def1=1, def2=[], def3='str'):?????????? # 定義一個有多個缺省參數(shù)的函數(shù)
...?def2.append(0)
...?print par1, def1, def2, def3
...
>>> fooM.func_defaults
(1, [], 'str')
? 在函數(shù)定義中有幾個缺省參數(shù),func_defaults中就會包括幾個對象,暫且稱之為缺省參數(shù)對象(如上列中的1,[]和'str')。這些缺省參數(shù)對象的生命周期與函數(shù)對象相同,從函數(shù)使用def定義開始,直到其消亡(如用del)。所以即便是在這些函數(shù)沒有被調(diào)用的時候,但只要定義了,缺省參數(shù)對象就會一直存在。
? 前面講過,函數(shù)調(diào)用的過程就是對象在另外一個名字空間的綁定過程。當(dāng)在每次函數(shù)調(diào)用時,如果沒有傳遞任何參數(shù)給這個缺省參數(shù),那么這個缺省參數(shù)的名字就會綁定到在func_defaults中一個對應(yīng)的缺省參數(shù)對象上。
>>> fooM(2)
? 函數(shù)fooM內(nèi)的名字def1就會綁定到func_defaults中的第一個對象,def2綁定到第二個,def3則是第三個。
所以我們看到在函數(shù)foo中出現(xiàn)的累加現(xiàn)象,就是由于par綁定到缺省參數(shù)對象上,而且它是一個可變對象(列表),par.append(0)就會每次改變這個缺省參數(shù)對象的內(nèi)容。
? 將函數(shù)foo改進(jìn)一下,可能會更容易幫助理解:
>>> def foo(par=[]):
...?print id(par)????????????????? # 查看該對象的標(biāo)識碼
...?par.append(0)
...?print par
...
>>> foo.func_defaults????????????????? # 缺省參數(shù)對象的初始值
([],)
>>> id(foo.func_defaults[0])?????????? # 查看第一個缺省參數(shù)對象的標(biāo)識碼
11279792?????????????????????????????? # 你的結(jié)果可能會不同
>>> foo()???????????????????????????????
11279792?????????????????????????????? # 證明par綁定的對象就是第一個缺省參數(shù)對象
[0]
>>> foo()
11279792?????????????????????????????? # 依舊綁定到第一個缺省參數(shù)對象
[0, 0]???????????????????????????????? # 該對象的值發(fā)生了變化
>>> b=[1]
>>> id(b)
11279952
>>> foo(b)???????????????????????????? # 不使用缺省參數(shù)
11279952?????????????????????????????? # 名字par所綁定的對象與外部名字b所綁定的是同一個對象
[1, 0]
>>> foo.func_defaults
([0, 0],)????????????????????????????? # 缺省參數(shù)對象還在那里,而且值并沒有發(fā)生變化
>>> foo()???????????????????
11279792?????????????????????????????? # 名字par又綁定到缺省參數(shù)對象上
([0, 0, 0],)
? 為了預(yù)防此類“問題”的發(fā)生,python建議采用下列方法:
>>> def foo(par=[]):
...?if par is None:
...??par = []
...?par.append(0)
...?print par
? 使用None作為哨兵,以判斷是否有參數(shù)傳入,如果沒有,就新創(chuàng)建一個新的列表對象,而不是綁定到缺省
參數(shù)對象上。
6.總結(jié)
? * python是一種純粹的面向?qū)ο笳Z言。
? * 賦值語句是名字和對象的綁定過程。
? * 函數(shù)的傳參是對象到不同名字空間的綁定。
7.參考資料
? * 《Dive Into Python》,Mark Pilgrim,http://diveintopython.org, 2003。
? * 《Python Objects》,F(xiàn)redrik Lundh,http://www.effbot.org/zone/python-objects.htm。
? * 《An Introduction to Python》,David M. Beazley,http://systems.cs.uchicago.edu/~beazley/tutorial/beazley_intro_python/intropy.pdf。
? *? 從Python官方網(wǎng)站(http://www.python.org)上可以了解到所有關(guān)于Python的知識。