物件參考、可變性與重複使用
- 「變數是標籤,不是盒子」
- 使用參考變數 (reference variable) 時,說「變數被指派給一個物件」會比較合理,畢竟——物件是在賦值之前建立的
- 兩個變數被指派到同一個物件時,這兩個變數互為「別名(alias)」
- 「每一個物件都有一個身份(ID)、一個型態跟一個值」,在 CPython,這個身份是
id()
,回傳物件的記憶體位置(不同解譯器可能會使用不同東西作為 ID) ==
比較物件的值;is
比較物件的 IDis
比==
快,因為它無法多載(不需要尋找或呼叫特殊方法來演算出一個值)- 原始物件的
__eq__
會比較 ID,但大多數覆寫__eq__
的情況通常會加入或使用別的比較 tuple
不可變的意思是「保存在它當中的物件參考 ID 不變」,即使tuple
可能存了可變的物件- 淺複製 (shallow copy) 即容器本身會被複製,但新的容器裡面保存的是舊的參考,例如
arr[:]
、arr.copy()
、copy(arr)
- 實作 deep copy 要小心物件可能會循環參考 (Ring),要判斷物件是否已經複製過
- 覆寫
__copy__
和__deepcopy__
可以控制copy.copy()
及copy.deepcopy()
的行為 - Python 函式傳遞的是參考(call by sharing) —— 即函數的參數 (parameter) 會指向引數 (argument) 的參考,換句話說,「函式內的參數就是其實際引數的別名」
- 同上,這也是為什麼「函式的預設參數不要使用可變型態」,簡單的改良:預設為 None,在函式中判斷是否初始化新的可變物件
del
刪除的是參考,而不是物件本身;物件只有在「參考數量變成零」的情況下才有可能被回收,這種銷毀可能不是立即性的- CPython 回收記憶體的演算法主要是計算參考數量,這個參考數量存在物件本身,但假若有循環參考時,容易發生 memory leak
- 在 CPython 的實作下,對
tuple
、str
、bytes
而言s[:]
不會製作複本,而是回傳物件的參考 - 在使用執行緒時,修改可變物件很難得到正確的結果:無法適當同步的執行序,會導致資料損毀;過度同步的執行序,會造成 deadlock
弱參考 (Weak Reference)
- 常用在使用快取的情境下,須要「參考一個不會被保存太久的物件」
- 弱參考是一種可呼叫的物件,它會回傳參考的物件,或者 None
- 使用弱參考而非賦值,就不會讓物件的「參考數量」增加
- 考慮使用
WeakKeyDictionary
、WeakValueDictionary
、WeakSet
與finalize
這些內部使用弱參考的高階界面,而非自己用weakref.ref
實作 - 因為實作的限制,
list
跟dict
的子類別可以被弱參考(原始型態不行),而int
、tuple
則完全無法被弱參考
1 2 3 4 5 6 7 8 9 10 11 12 13 |
>>> import weakref >>> a_set = {0, 1} >>> wref = weakref.ref(a_set) >>> wref() {0, 1} >>> a_set = {2,3,4} >>> wref() {0, 1} >>> wref() is None False >>> wref() is None True |
字串常值的共用,是一種優化技術,稱為 interning,Cpython 會對小型的整數使用相同的技術,來避免沒必要的重複
1 2 3 4 5 |
>>> s1 = 'ABC' >>> s2 = 'ABC' >>> s1 is s2 True |
Pythonic Object
- 四種字串表示的特殊方法:
__repr__
、__str__
、__format__
、__bytes——
分別對應內建函式repr()
、str()
、bytes()
、format()
- repr 是給開發者看的;str 是給使用者看的
類別實例與 bytes 相互轉換的實作範例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
from array import array class Vector2d: typecode = 'd' # 8-bytes per unit def __init__(self, x, y): self.x, self.y = x, y def __iter__(self): return (i for i in (self.x, self.y)) def __bytes__(self): return (bytes([ord(self.typecode)]) + bytes(array(self.typecode, self))) @classmethod def frombytes(cls, octets): typecode = chr(octets[0]) memv = memoryview(octets[1:]).cast(typecode) return cls(*memv) >>> v1 = Vector2d(1, -1) >>> bytes(v1) b'd\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0\xbf' >>> v2 = Vector2d.frombytes(bytes(v1)) |
classmethod
的第一個引數一定是類別自己(通常命名為cls
),而staticmethod
則是跟一般函式沒兩樣(不是很實用)format(obj, format_spec)
接受格式指定符(formatting specifier) 作為引數,其使用的語法稱為 Format Specification Mini-Language- Format Specification Mini-Language 是可以擴充的,每個類別都可以按照自己喜歡的方式解釋 format_spect 引數,但沒有實作
__format__
的話,則回傳str()
且不接受指定符參數 - 拿來做
__hash__
的物件屬性最好具有唯獨保護(實例的雜湊值永遠不該改變);在實作上,建議使用 XOR 運算子來混合物件屬性本身的雜湊,例如hash(self.x) ^ hash(self.y)
- 在實例屬性命名時,加上前綴兩個底線,在存取會變成例如
_Dog__mood
,這種功能叫做名稱重整 (name mangling),用意是防止有人意外存取,或是不當覆寫——這作法並不受歡迎,還不如單純用一個底線當前綴就好(反正你知我知大家知不要去動它) __slot__
會讓解譯器將實例以 tuple 保存而不是 dict,藉此節省記憶體開銷,但此設定無法被繼承,適合用在表格資料、結構描述(schema)被定死且資料集眾多的資料庫紀錄上- 實例只能擁有
__slot__
指定的屬性,但不要笨笨的在裡面加上__dict__
那樣就沒屁用了 - 「到處使用 getter/setter 呼叫式很蠢」—— 在 Python,單純使用公開屬性就好,如果須要,可以把它們改成 property 來做寫入保護
- Java 的
private
與protected
修飾器通常只能防止不小心的行為(即安全性),它們只能在應用程式使用 security manager 來部署時(通常很少),才可以保證能夠防止惡意入侵
序列修改、雜湊、切片
- 實作容納大量項目的集合型態,一個重點是簡寫
repr()
的輸出,可以直接現有的工具reprlib.repr
來做 - 在物件導向的環境中,協定 (protocal) 是一種非正式的界面,只會被定義在文件中,而不會被定義在程式中。例如:Python 的序列協定只涉及
__len__
與__getitem__
,我們不用明確宣告它是序列,只要它有序列的行為就夠了,這就是 duck typing
一個好的 __getitem__
實作範例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class CustomSequence: def __init__(self, components): self._components = components def __getitem__(self, index): cls = type(self) if isinstance(index, slice): return cls(self._components[index]) elif isinstance(index, numbers.Intergal): return self.components[index] else: msg = '{cls.__name__} indices must be integers' raise TypeError(msg.format(cls=cls)) |
- 要製作 Pythonic 的物件,就要模擬 Python 自己的物件——「模擬的程度只要合理地符合被塑造的物件就可以了」、「只實作部份協定可以讓事情保持簡單」
- 當你實作
__getattr__
時,通常也要一起實作__setattr__
來避免物件之間有不一致的行為 - 使用
reduce
時,提供第三個引數 (initializer) 作為初始值可以避免因傳入空序列而導致的例外 map
、zip
回傳的的都是 generatorzip
的命名來自拉鍊 (zipper fastener)- 「如果你想要總和一串項目,就應該在編寫時,讓它看起來像『一串項目的總和』,而不是像『對這些項目執行迴圈,使用令一個變數,執行一系列的加法』」——僅作為參考用,每個人心中對 “idiomatic Python” 有不同的認知