變數全都是引用
跟其他程式語言不同,Python的變數不是盒子,不會儲存資料,它們只是引用,就像標籤一樣,貼在物件上面。
比如:
>>> a = [1, 2, 3]
>>> b = a
>>> a.append(4)
>>> b
[1, 2, 3, 4]
>>> b is a
True
a變數和b變數引用的是同一個列表[1, 2, 3]
。b可以叫做a的別名。
比較來看:
>>> a = [1, 2, 3]
>>> c = [1, 2, 3]
>>> c == a
True
>>> c is a
False
c引用的是另外一個列表,雖然和a引用的列表的值相等,但是它們是不同的物件。
淺複製與深複製
淺複製是指只複製最外層容器,副本中的元素是源容器中元素的引用。如果所有元素都是不可變的,那麼這樣沒有問題,還能節省內容。但是,如果有可變的元素,那麼結果可能會出乎意料之外。構造方法或[:]
做的都是淺複製。
示例:
>>> x1 = [3, [66, 55, 44], (7, 8, 9)]
# x2是x1的淺複製
>>> x2 = list(x1)
# 不可變元素沒有影響
>>> x1.append(100)
>>> x1
[3, [66, 55, 44], (7, 8, 9), 100]
>>> x2
[3, [66, 55, 44], (7, 8, 9)]
# x1[1]是列表,可變元素會影響x2
# 因為它們引用的是同一個物件
>>> x1[1].remove(55)
>>> x1
[3, [66, 44], (7, 8, 9), 100]
>>> x2
[3, [66, 44], (7, 8, 9)]
# x2[1]也會反過來影響x1
>>> x2[1] += [33, 22]
>>> x1
[3, [66, 44, 33, 22], (7, 8, 9), 100]
>>> x2
[3, [66, 44, 33, 22], (7, 8, 9)]
# 不可變元組也不會有影響
# +=運算子建立了一個新元組
>>> x2[2] += (10, 11)
>>> x1
[3, [66, 44, 33, 22], (7, 8, 9), 100]
>>> x2
[3, [66, 44, 33, 22], (7, 8, 9, 10, 11)]
深複製是指我們常規理解的複製,副本不共享內部物件的引用,是完全獨立的一個副本。這可以藉助copy.deepcopy來實現。
示例:
>>> a = [10, 20]
>>> b = [a, 30]
>>> a.append(b)
>>> a
[10, 20, [[...], 30]]
>>> from copy import deepcopy
>>> c = deepcopy(a)
>>> c
[10, 20, [[...], 30]]
即使是有迴圈引用也能正確複製。
注意copy.copy()是淺複製,copy.deepcopy()是深複製。
函式傳參
Python唯一支援的引數傳遞模式是共享傳參,也就是指函式的各個形式引數獲得實參中各個引用的副本。因為Python的變數全都是引用。對於不可變物件來說沒有問題,但是對於可變物件就不一樣了。
示例:
>>> def f(a, b):
... a += b
... return a
...
# 數字不變
>>> x = 1
>>> y = 2
>>> f(x, y)
3
>>> x, y
(1, 2)
# 列表變了
>>> a = [1, 2]
>>> b = [3, 4]
>>> f(a, b)
[1, 2, 3, 4]
>>> a, b
([1, 2, 3, 4], [3, 4])
# 元組不變
>>> t = (10, 20)
>>> u = (30, 40)
>>> f(t, u)
(10, 20, 30, 40)
>>> t, u
((10, 20), (30, 40))
由此可以得出一條警示:函式引數儘量不要使用可變引數,如果非用不可,應該考慮在函式內部進行復制。
示例:
class TwilightBus:
"""A bus model that makes passengers vanish"""
def __init__(self, passengers=None):
if passengers is None:
self.passengers = []
else:
self.passengers = passengers
def pick(self, name):
self.passengers.append(name)
def drop(self, name):
self.passengers.remove(name)
測試一下:
>>> basketball_team = ['Sue', 'Tina', 'Maya', 'Diana', 'Pat']
>>> bus = TwilightBus(basketball_team)
>>> bus.drop('Tina')
>>> bus.drop('Pat')
>>> basketball_team
['Sue', 'Maya', 'Diana']
TwilightBus下車的學生,竟然從basketball_team中消失了。這是因為self.passengers引用的是同一個列表物件。修改方法很簡單,複製個副本:
def __init__(self, passengers=None):
if passengers is None:
self.passengers = []
else:
self.passengers = list(passengers) # 使用建構函式複製副本
del和垃圾回收
del語句刪除的是引用,而不是物件。但是del可能會導致物件沒有引用,進而被當做垃圾回收。
示例:
>>> import weakref
>>> s1 = {1, 2, 3}
# s2和s1引用同一個物件
>>> s2 = s1
>>> def bye():
... print("Gone")
...
# 監控物件和呼叫回撥
>>> ender = weakref.finalize(s1, bye)
>>> ender.alive
True
# 刪除s1後還存在s2引用
>>> del s1
>>> ender.alive
True
# s2重新繫結導致{1, 2, 3}引用歸零
>>> s2 = "spam"
Gone
# 物件被銷燬了
>>> ender.alive
False
在CPython中,物件的引用數量歸零後,物件會被立即銷燬。如果除了迴圈引用之外沒有其他引用,兩個物件都會被銷燬。
弱引用
某些情況下,可能需要儲存物件的引用,但不留存物件本身。比如,有個類想要記錄所有例項。這個需求可以使用弱引用實現。
比如上面示例中的weakref.finalize(s1, bye),finalize就持有{1, 2, 3}
的弱引用,雖然有引用,但是不會影響物件被銷燬。
其他使用弱引用的方式是WeakDictionary、WeakValueDictionary、WeakSet。
示例:
class Cheese:
def __init__(self, kind):
self.kind = kind
def __repr__(self):
return 'Cheese(%r)' % self.kind
>>> import weakref
>>> stock = weakref.WeakValueDictionary()
>>> catalog = [Cheese('Red Leicester'), Cheese('Tilsit'),
... Cheese('Brie'), Cheese('Parmesan')]
...
>>> for cheese in catalog:
# 用作快取
# key是cheese.kind
# value是cheese的弱引用
... stock[cheese.kind] = cheese
...
>>> sorted(stock.keys())
['Brie', 'Parmesan', 'Red Leicester', 'Tilsit']
# 刪除catalog引用,stock弱引用不影響垃圾回收
# WeakValueDictionary的值引用的物件被銷燬後,對應的鍵也會自動刪除
>>> del catalog
>>> sorted(stock.keys()) # 還存在一個cheese臨時變數的引用
['Parmesan']
# 刪除cheese臨時變數的引用,stock就完全清空了
>>> del cheese
>>> sorted(stock.keys())
[]
注意不是每個Python物件都可以作為弱引用的目標,比如基本的list和dict就不可以,但是它們的子類是可以的:
class MyList(list): pass a_list = MyList(range(10)) weakref_to_a_list = weakref.ref(a_list)
小結
本文首先闡述了Python變數全部都是引用的這個事實,這意味著在Python中,簡單的賦值是不建立副本的。如果要建立副本,可以選擇淺複製和深複製,淺複製使用構造方法、[:]
或copy.copy()
,深複製使用copy.deepcopy()
。del刪除的是引用,但是會導致物件沒有引用而被當做垃圾回收。有時候需要保留引用而不保留物件(比如快取),這叫做弱引用,weakref庫提供了相應的實現。
參考資料:
《流暢的Python》