- 原文地址:Classes Without Classes
- 原文做者:Fuyukai
- 譯文出自:掘金翻譯計劃
- 本文永久連接:github.com/xitu/gold-m…
- 譯者:EmilyQiRabbit
- 校對者:allenlongbaobao,sunhaokk
Python 的對象模型使人難以置信的強大;實際上,你能夠重寫全部(對象),或者向任何人分發奇怪的對象,並讓他們像對待正常的對象的那樣接受它。前端
Python 的面向對象是 smalltalk 面向對象的一個後裔。在 Python 中,一切都是對象,甚至對象集和對象類型都是如此;特別的,函數也是對象。這讓我很好奇:不使用類建立一個類是否可能?android
這個想法的關鍵性代碼以下所示。這是一個很基礎的實現,但它支持 __call__
這樣的邊緣狀況(但不支持其餘魔術方法,由於他們須要加載依賴)。後文將會解說。ios
這是一些很先進的 Python 輪子,它用一種和對象的設計初衷毫不相同的方法使用了一些對象。咱們將分段解說代碼。git
def _suspend_self(namespace, suspended):
複製代碼
這是個讓人有點懼怕的函數名。暫停?這可很差,但咱們是能夠解決問題的。_suspend_self
函數是 functools.partial
的一個簡單應用,它的工做原理是:經過從外部函數做用域中捕獲 namespace
,並把它懸停在內部函數中。github
def suspender(*args, **kwargs):
return suspended(namespace, *args, **kwargs)
複製代碼
接下來,這個內部的函數調用了和第一個參數 namespace 一塊兒傳遞進來的函數 suspended,實際上這是將方法又包了一層,這樣它就能夠應用在一個普通的 Python 類上。_suspend_self
餘下的部分就只是設置一些屬性,這些屬性在某些時候可能會被映射(reflection)用到(我可能漏掉一些內容)。後端
下一個函數是 make_class
。從它的簽名中咱們能知道什麼?bash
def make_class(locals: dict):
""" 在被調用者的本地建立一個類。 參數 locals:創建類的本地。 """
複製代碼
若是其餘方法請求或者直接取得了你的本地變量,可不是什麼好事。一般狀況下,這是爲了在以前的棧中搜索什麼東西,或者就是在黑你的本機。咱們當前的實例屬於前面一種,搜索本地函數並加入到類中。函數
# 試着找到一個 `__call__` 來執行 call 函數
# 它將做爲一個函數,這樣命名空間和被調用者能夠引用彼此
def call_maker():
if '__call__' in locals and callable(locals['__call__']):
return _suspend_self(namespace, locals['__call__'])
def _not_callable(*args, **kwargs):
raise TypeError('This is not callable')
return _not_callable
複製代碼
這個函數至關簡單,它是一個將函數做爲返回值的函數! 它實際上作了以下這些事:區塊鏈
__call__
_suspend_self
函數「掛載」 namespace 來用 __call__
生成一個方法。__call__
同樣,返回一個會發起錯誤的樁函數(stub function)。namespace 是關鍵的部分,然而我尚未解說。類中的每個(或者絕大部分)方法都會將 self
做爲第一個參數,這個 self
就是函數運行的時候類的實例。ui
一個類的實例實際上就是一個你能夠用 .
符號而不是數字索引訪問其內容的字典。因此須要一個能夠傳入咱們指望的函數的對象來模仿這個字典。因而咱們就說,這個實例是一個 namespace
,咱們在 namespace
上設置變量等等。後文提到 namespace
的地方,就把它看成咱們的實例。經過調用類的對象自身,你能夠獲取這個類的實例:obb = SomeClass()
。
標準的建立點式訪問的字典的方法是 attrdict:
attrdict = type("attrdict", (dict,), {"__getattr__": dict.__getitem__, "__setattr__": dict.__setitem__})
複製代碼
可是既然它建立了一個類,這就有點欺騙性了。其餘的方法包括 typing.SimpleNamespace
,或者建立一個無哨兵(sentinel)的類。可是這兩種方法都仍是欺騙性的建立了類,咱們都不能用。
namespace 的解決方案是另外一個函數。函數的行爲能夠像可調用的點式訪問字典,因此咱們就簡單的建立一個 namespace
函數,假設它就是 self。
# 這個就充當了 self 對象
# 全部的屬性都創建在此之上
def namespace():
return called()
複製代碼
須要注意調用 called()
的用法 - 這是爲了正常模擬實例上 __call__
的行爲。
__init__
Python 中的全部類都有 __init__
(不包括默認提供空 init 的類),因此咱們須要去模仿這一點並確保用戶定義的 init 被調用。
# 建立一個 init 的替代方法
def new_class(*args, **kwargs):
init = locals.get("__init__")
if init is not None:
init(namespace, *args, **kwargs)
return namespace
複製代碼
這段代碼就是簡單的從本地獲取用戶定義的 __init__
,若是找到了,就調用它。而後,它返回 namespace(就是假的實例),有效地模擬了循環:(metaclass.)__call__
-> __new__
-> __init__
。
接下來要作的就是在類的基礎上建立方法,這能夠用超級簡單的循環掃描來完成:
# 更新 namespace
for name, item in locals.items():
if callable(item):
fn = _suspend_self(namespace, item)
setattr(namespace, name, fn)
複製代碼
和上文提到的類似,全部可調用的函數都被 _suspend_self
包裹來將函數變成類的方法,在 namespace 完成設置。
最後要作的就是簡單的 return new_class
。獲取到類的實例的最後一輪循環是:
make_class
來設置 namespace(添加 @make
修飾符,這一步就能自動完成)make_class
函數設置實例,使其爲後續的初始化作好準備make_class
函數返回另外一個函數,調用這個函數就能獲取到實例並完成它的初始化。如今咱們就獲得它了,一個徹底沒用類的類。打賭你會實際應用它。
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。