目錄
對象變動(Mutation)
__slots__魔法
Python中可變(mutable)與不可變(immutable)的數據類型讓新手很是頭痛。簡單的說,可變(mutable)意味著"可以被改動",而不可變(immutable)的意思是“常量(constant)”。想把腦筋轉動起來嗎?考慮下這個例子:
foo = ['hi']
print(foo)
# Output: ['hi']
bar = foo
bar += ['bye']
print(foo)
# Output: ['hi', 'bye']
剛剛發生了什麼?我們預期的不是那樣!我們期望看到是這樣的:
foo = ['hi']
print(foo)
# Output: ['hi']
bar = foo
bar += ['bye']
print(foo)
# Output: ['hi']
print(bar)
# Output: ['hi', 'bye']
這不是一個bug。這是對象可變性(mutability)在作怪。每當你將一個變量賦值為另一個可變類型的變量時,對這個數據的任意改動會同時反映到這兩個變量上去。新變量只不過是老變量的一個別名而已。這個情況只是針對可變數據類型。下面的函數和可變數據類型讓你一下就明白了:
def add_to(num, target=[]):
target.append(num)
return target
add_to(1)
# Output: [1]
add_to(2)
# Output: [1, 2]
add_to(3)
# Output: [1, 2, 3]
你可能預期它表現的不是這樣子。你可能希望,當你調用add_to時,有一個新的列表被創建,就像這樣:
def add_to(num, target=[]):
target.append(num)
return target
add_to(1)
# Output: [1]
add_to(2)
# Output: [2]
add_to(3)
# Output: [3]
啊哈!這次又沒有達到預期,是列表的可變性在作怪。在Python中當函數被定義時,默認參數只會運算一次,而不是每次被調用時都會重新運算。你應該永遠不要定義可變類型的默認參數,除非你知道你正在做什麼。你應該像這樣做:
def add_to(element, target=None):
if target is None:
target = []
target.append(element)
return target
現在每當你在調用這個函數不傳入target參數的時候,一個新的列表會被創建。舉個例子:
add_to(42)
# Output: [42]
add_to(42)
# Output: [42]
add_to(42)
# Output: [42]
在Python中,每個類都有實例屬性。默認情況下Python用一個字典來保存一個對象的實例屬性。這非常有用。因為它允許我們在運行時去設置任意的新屬性。
然而,對於有著已知屬性的小類來說,它可能是個瓶頸。這個字典浪費了很多內存。
Python不能在對象創建時直接分配一個固定量的內存來保存所有的屬性。因此如果你創建許多對象(我指的是成千上萬個),它會消耗掉很多內存。
不過還是有一個方法來規避這個問題。這個方法需要使用__slots__來告訴Python不要使用字典,而且只給一個固定集合的屬性分配空間。
這裡是一個使用與不使用__slots__的例子:
不使用__slots__:
class MyClass(object):
def __init__(self, name, identifier):
self.name = name
self.identifier = identifier
self.set_up()
使用__slots__:
class MyClass(object):
__slots__ = ['name', 'identifier']
def __init__(self, name, identifier):
self.name = name
self.identifier = identifier
self.set_up()
第二段代碼會為你的內存減輕負擔。通過這個技巧,有些人已經看到內存占用率幾乎40%~50%的減少。
*稍微備注一下,你也許需要試一下PyPy。它已經默認地做了所有這些優化。
以下你可以看到一個例子,它用IPython來展示在有與沒有__slots__情況下的精確內存
占用。
Python 3.4.3 (default, Jun 6 2015, 13:32:34)
Type "copyright", "credits" or "license" for more information.
IPython 4.0.0 -- An enhanced Interactive Python.
? -> Introduction and overview of IPython's features.
%quickref -> Quick reference.
help -> Python's own help system.
object? -> Details about 'object', use 'object??' for extra details.
In [1]: import ipython_memory_usage.ipython_memory_usage as imu
In [2]: imu.start_watching_memory()
In [2] used 0.0000 MiB RAM in 5.31s, peaked 0.00 MiB above current, total RAM
In [3]: %cat slots.py
class MyClass(object):
__slots__ = ['name', 'identifier']
def __init__(self, name, identifier):
self.name = name
self.identifier = identifier
num = 1024*256
x = [MyClass(1,1) for i in range(num)]
In [3] used 0.2305 MiB RAM in 0.12s, peaked 0.00 MiB above current, total RAM
In [4]: from slots import *
In [4] used 9.3008 MiB RAM in 0.72s, peaked 0.00 MiB above current, total RAM
In [5]: %cat noslots.py
class MyClass(object):
def __init__(self, name, identifier):
self.name = name
self.identifier = identifier
num = 1024*256
x = [MyClass(1,1) for i in range(num)]
In [5] used 0.1758 MiB RAM in 0.12s, peaked 0.00 MiB above current, total RAM
In [6]: from noslots import *
In [6] used 22.6680 MiB RAM in 0.80s, peaked 0.00 MiB above current