置方法和屬性詳解)
前言Python 里凡是名字前后各帶兩條下劃線的東西例如__init__、__repr__、__dict__社區(qū)俗稱「魔術(shù)方法」magic method或 dunderdouble underscore 的縮寫。它們不是給程序員隨便調(diào)用的而是語言本身在特定時刻會去調(diào)用的鉤子寫len(obj)時解釋器實際調(diào)用的是obj.__len__()寫obj other時調(diào)用的是obj.__add__(other)。圍繞它們有兩個典型誤解。第一把「內(nèi)置方法」和「內(nèi)置函數(shù)」混為一談——len()是內(nèi)置函數(shù)__len__是你可以在自己類里實現(xiàn)的協(xié)議方法兩者是調(diào)用者與被調(diào)用者的關(guān)系。第二以為__str__不定義就沒法打印——實際上print(obj)會退而調(diào)用__repr__而__repr__由object提供了一個默認實現(xiàn)只是輸出不好看而已。本文講清屬性attribute與方法method兩套機制給出能直接運行的示例并說明哪些行為是文檔保證的、哪些只是當(dāng)前實現(xiàn)的細節(jié)。示例基于 Python 3.8。一、先分清「屬性」與「方法」在 Python 的對象模型里一切都在字典里。實例屬性存在__dict__方法本質(zhì)上就是存在類字典里的函數(shù)對象通過描述符協(xié)議在訪問時綁定出self。名字類型含義在哪里__dict__屬性對象/類的屬性字典實例與類都有__class__屬性所屬類實例__name__屬性名字類、函數(shù)、模塊__doc__屬性文檔字符串類、函數(shù)、模塊__module__屬性定義所在的模塊名類、函數(shù)__qualname__屬性限定名含外層類名類、函數(shù)3.3__bases__屬性直接父類元組類__mro__屬性方法解析順序類__slots__屬性聲明式屬性表省內(nèi)存類可選# 適用于 Python 3.8class Point:二維點。dims 2def __init__(self, x, y):self.x xself.y yp Point(1, 2)print(p.__dict__) # {x: 1, y: 2}print(p.__class__) # class __main__.Pointprint(Point.__name__) # Pointprint(Point.__doc__) # 二維點。print(Point.__bases__) # (class object,)print(Point.dims, p.dims)# 2 2類屬性實例也能讀到注意p.__dict__里沒有dims——它是類屬性。找屬性時先查實例字典再順著__mro__查類字典。給實例賦值p.dims 3會新建一個實例屬性遮蔽類屬性不影響別的實例。二、對象的構(gòu)造與表示方法何時被調(diào)用返回值約定__new__(cls, ...)創(chuàng)建實例最先執(zhí)行返回實例__init__(self, ...)初始化實例必須返回None__repr__(self)repr()、交互式回顯、調(diào)試str盡量可還原__str__(self)str()、print()str面向人__format__(self, spec)f-string 與format()str__hash__(self)hash()、放進set/dict鍵int__eq__(self, other)任意按約定給bool# 適用于 Python 3.8class Money:def __init__(self, amount, currencyCNY):self.amount amountself.currency currencydef __repr__(self):return fMoney({self.amount!r}, {self.currency!r})def __str__(self):return f{self.amount} {self.currency}m Money(12.5)print(repr(m)) # Money(12.5, CNY)print(str(m)) # 12.5 CNYprint(m) # 12.5 CNYprint 用的是 __str__print([m]) # [Money(12.5, CNY)]容器用的是 __repr__經(jīng)驗法則__repr__給開發(fā)者看越像構(gòu)造表達式越好__str__給最終用戶看。只定義__repr__時str()會自動回退到它。三、容器協(xié)議與迭代協(xié)議想讓自定義對象支持len()、下標(biāo)、for循環(huán)、in判斷就實現(xiàn)對應(yīng)協(xié)議。方法觸發(fā)它的語法__len__len(obj)__getitem__obj[key]__setitem__obj[key] v__delitem__del obj[key]__contains__x in obj__iter__for x in obj、iter(obj)__next__next(obj)__call__obj(...)__enter__/__exit__with obj:# 適用于 Python 3.8class Countdown:def __init__(self, start):self.start startdef __iter__(self):n self.startwhile n 0:yield nn - 1def __len__(self):return self.startcd Countdown(3)print(len(cd)) # 3print(list(cd)) # [3, 2, 1]class Multiplier:def __init__(self, factor):self.factor factordef __call__(self, x):return x * self.factordouble Multiplier(2)print(double(21)) # 42print(callable(double)) # True只要定義了__call__實例就能像函數(shù)一樣被調(diào)用callable()也會返回True。四、哪些行為是保證的__dict__的存在、__mro__的線性化順序、屬性查找順序都是文檔保證的。但下面這些屬于實現(xiàn)細節(jié)實例屬性在__dict__里的順序CPython 中通常是插入順序但不應(yīng)依賴__slots__能省內(nèi)存但它會去掉實例的__dict__一旦定義就只能給列出的名字賦值無法再動態(tài)添加屬性定義了__eq__卻沒定義__hash__Python 會把該類的__hash__置為None實例立即變成不可哈希、不能放進set或當(dāng)字典鍵。# 適用于 Python 3.8class Item:def __init__(self, sku):self.sku skudef __eq__(self, other):return isinstance(other, Item) and self.sku other.sku__hash__ None # 顯式寫出這個事實print(Item(A) Item(A)) # True# {Item(A)} # TypeError: unhashable type常見坑點坑 1__init__里寫了return。?class C:def __init__(self):return 1?__init__必須返回None或不寫 return否則實例化時拋TypeError: __init__() should return None。想控制返回值要改__new__???2只寫__str__就以為調(diào)試輸出夠用。? 容器、交互式回顯用的都是__repr__print(list_of_obj)里全是默認的內(nèi)存地址形式形如 object at 0x 加一串十六進制。? 至少實現(xiàn)__repr__再按需實現(xiàn)__str__???3定義__eq__后忘記__hash__。? 對象放進set或當(dāng)dict鍵時報unhashable type。? 要么一起實現(xiàn)__hash__要么明確該類設(shè)計為不可哈希。坑 4__eq__里沒做類型檢查。?def __eq__(self, other):return self.sku other.sku # other 可能是任何類型? 先if not isinstance(other, Item): return NotImplemented讓解釋器有機會走對方的比較邏輯???5以為self.__x是私有的。? 以為雙下劃線能阻止外部訪問。? 雙下劃線只做名字改寫name mangling在類C里寫self.__x實際是self._C__x仍然能被訪問。約定上受保護用單下劃線_x表示但它同樣只是約定???6給類加__slots__后還想動態(tài)加屬性。? 定義__slots__ (x,)之后寫obj.y 1拋AttributeError。? 把y也列進__slots__或在__slots__里保留__dict__這樣就不再省內(nèi)存了???7__len__返回了非整數(shù)。?def __len__(self): return 3——len(obj)拋TypeError。? 返回int且非負bool(obj)也可以用__len__的結(jié)果為 0 即假所以別讓__len__有副作用???8在__repr__里訪問會觸發(fā)大量計算或 IO 的屬性。?__repr__里讀數(shù)據(jù)庫、打印大對象全量內(nèi)容導(dǎo)致調(diào)試時更卡。?__repr__保持輕量、無副作用只輸出標(biāo)識性的少量字段??偨Y(jié)類別代表方法 / 屬性一句話提醒屬性__dict__、__class__、__doc__、__mro__查找順序?qū)嵗值?→ 類 MRO構(gòu)造__new__、__init____init__必須返回None表示__repr__、__str__、__format__至少實現(xiàn)__repr__比較與哈希__eq__、__hash__定義__eq__會連帶清空__hash__容器與迭代__len__、__getitem__、__iter__、__contains__協(xié)議齊全才寫得順手調(diào)用與上下文__call__、__enter__、__exit__with語句靠這兩個理解 dunder 的關(guān)鍵是轉(zhuǎn)換視角它們不是要背的方法名而是語言在某個語法被使用時去找的鉤子。想知道某個類實現(xiàn)了哪些鉤子直接print(類名.__mro__)再help(類名)或者讀object的文檔不要憑印象猜哪個魔術(shù)方法存在、參數(shù)是不是self, other。本主題與 Python 2 的主要差別在于Python 2 里存在__unicode__、__nonzero__、__cmp__這些方法在 Python 3 中分別被__str__、__bool__、__eq__體系取代。Python 2.7 已于 2020 年 1 月 1 日停止維護新代碼不要再用舊方法名。