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

我們可以看到,改變ab也跟著變,因為他們始終指向同一個地址

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編程

專欄目錄:目錄

版本說明:軟體及包版本說明


推薦閱讀:

TAG:Python | Python入門 | 數據類型 |