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