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