標籤:對象 table -o 出現 ... 魔法方法 參考 可變參數 self
本文分為如下幾個部分
- 概念
- 地址問題
- 作為函數參數
- 可變參數在類中使用
- 函數預設參數
- 類的實現上的差異
概念
可變對象與不可變對象的區別在於對象本身是否可變。
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入門之python可變對象與不可變對象