python可變對象與不可變對象
本文分為如下幾個部分
- 概念
- 地址問題
- 作為函數參數
- 可變參數在類中使用
- 函數默認參數
- 類的實現上的差異
概念
可變對象與不可變對象的區別在於對象本身是否可變。
python內置的一些類型中
- 可變對象:list dict set
- 不可變對象:tuple string int float bool
舉一個例子
# 可變對象>>> a = [1, 2, 3]>>> a[1] = 4>>> a[1, 4, 3]# 不可變對象>>> b = (1, 2, 3)>>> b[1] = 4Traceback (most recent call last): File "<stdin>", line 1, in <module>TypeError: tuple object does not support item assignment
上面例子很直觀地展現了,可變對象是可以直接被改變的,而不可變對象則不可以
地址問題
下面我們來看一下可變對象的內存地址變化
>>> a = [1, 2, 3]>>> id(a)2139167175368>>> a[1] = 4>>> id(a)2139167175368
我們可以看到,可變對象變化後,地址是沒有改變的
如果兩個變數同時指向一個地址
1.可變對象
>>> a = [1, 2, 3]>>> id(a)2139167246856>>> b = a>>> id(b)2139167246856>>> a[1] = 4>>> a[1, 4, 3]>>> b[1, 4, 3]>>> id(a)2139167246856>>> id(b)2139167246856
我們可以看到,改變a
則b
也跟著變,因為他們始終指向同一個地址
2.不可變對象
>>> a = (1, 2, 3)>>> id(a)2139167074776>>> b = a>>> a = (4, 5, 6)>>> a(4, 5, 6)>>> b(1, 2, 3)>>> id(a)2139167075928>>> id(b)2139167074776
我們可以看到,a
改變後,它的地址也發生了變化,而b
則維持原來的地址,原來地址中的內容也沒有發生變化
作為函數參數
1.可變對象
>>> def myfunc(l):... l.append(1)... print(l)...>>> l = [1, 2, 3]>>> myfunc(l)[1, 2, 3, 1]>>> l[1, 2, 3, 1]
我們可以看到,可變對象作為參數傳入時,在函數中對其本身進行修改,是會影響到全局中的這個變數值的,因為函數直接對該地址的值進行了修改。
2.不可變對象
>>> def myfunc(a):... a += 1... print(a)...>>> a = 2>>> myfunc(a)3>>> a2
對於不可變對象來說,雖然函數中的a
值變了,但是全局中的a
值沒變,因為函數中的a
值已經對應了另外一個地址,而全局中的a
值指向的原來地址的值是沒有變的。
3.總結
python中向函數傳遞參數只能是引用傳遞,表示把它的地址都傳進去了,這才會帶來上面的現象。
有的編程語言允許值傳遞,即只是把值傳進去,在裡面另外找一個地址來放,這樣就不會影響全局中的變數。
可變參數在類中使用
我們直接來看下面這個例子
class Myclass: def __init__(self, a): self.a = a def printa(self): print(self.a)
運行如下
>>> aa = [1,2]>>> my = Myclass(aa)>>> my.printa()[1, 2]>>> aa.append(3)>>> my.printa()[1, 2, 3]
我們可以看到,類中的變數和全局變數地址依然是共用的,無論在哪裡修改都會影響對方。
其實這個特性也不能說是一個弊端,利用這一點可以進行一些很方便的操作,比如兩個線程同時操作一個隊列,我們不用設置一個global
隊列,只要將隊列這個可變對象傳入類之中,修改就會自動同步。
下面這個生產者消費者例子就是這樣
import timeimport threadingimport randomfrom queue import Queueclass Producer(threading.Thread): def __init__(self, queue): threading.Thread.__init__(self) self.queue = queue def run(self): while True: random_integer = random.randint(0, 100) self.queue.put(random_integer) print(add {}.format(random_integer)) time.sleep(random.random())class Consumer(threading.Thread): def __init__(self, queue): threading.Thread.__init__(self) self.queue = queue def run(self): while True: get_integer = self.queue.get() print(lose {}.format(get_integer)) time.sleep(random.random())def main(): queue = Queue() th1 = Producer(queue) th2 = Consumer(queue) th1.start() th2.start()if __name__ == __main__: main()
將queue
傳入兩個類中,在兩個類中隨意更改,自動在兩個類間同步。
函數默認參數
函數默認參數一定要設定為不可變參數,否則會引發一些錯誤,我們來看下面一個例子
>>> def myfunc(l=[]):... l.append(add)... print(l)...>>> myfunc([1, 2, 3])[1, 2, 3, add]>>> myfunc([a, b])[a, b, add]
上面代碼是正常運行的,我們來看下面這些
>>> myfunc()[add]>>> myfunc()[add, add]>>> myfunc()[add, add, add]
按理說應該每次都是[add]
,但是現在出現了意想不到的錯誤。
這是因為l = []
是在函數定義時就確定下來的了,所以之後每次調用這個函數,使用的l
都是同一個,如果不指定這個參數的新值,就會出現上面這個問題。
上面這個l
可以默認設置為None
,這就是一個不可變對象。
類的實現上的差異
其實list tuple里的這些元素都相當於類的屬性,修改他們相當於修改類的屬性。
正常定義一個類它的屬性是可以正常訪問和修改的,所以那些類的實例都是可變對象。
我們只要定義一個類,不允許它修改屬性,就可以創建一個不可變對象。
這就要使用python的魔法方法,主要有兩種方法
- 設置
__setattr__
直接拋出異常,即只要想設置屬性值,就會拋出異常 - 設置
__slot__
限制屬性的訪問,如果屬性都不能訪問了,那就肯定不能修改
更細節的實現可以參考stackoverflow上的回答
專欄信息
專欄主頁:python編程
專欄目錄:目錄
版本說明:軟體及包版本說明
推薦閱讀: