- 原文地址:Classes Without Classes
- 原文作者:Fuyukai
- 譯文出自:掘金翻譯計劃
- 本文永久連結:github.com/xitu/gold-m…
- 譯者:EmilyQiRabbit
- 校對者:allenlongbaobao,sunhaokk
不用 Class,如何寫一個類
前言
Python 的物件模型令人難以置信的強大;實際上,你可以重寫所有(物件),或者向任何人分發奇怪的物件,並讓他們像對待正常的物件的那樣接受它。
Python 的物件導向是 smalltalk 物件導向的一個後裔。在 Python 中,一切都是物件,甚至物件集和物件型別都是如此;特別的,函式也是物件。這讓我很好奇:不使用類建立一個類是否可能?
程式碼
這個想法的關鍵性程式碼如下所示。這是一個很基礎的實現,但它支援 __call__
這樣的邊緣情況(但不支援其他魔術方法,因為他們需要載入依賴)。後文將會解說。
有沒有搞錯?
這是一些很先進的 Python 輪子,它用一種和物件的設計初衷絕不相同的方法使用了一些物件。我們將分段解說程式碼。
第一個 helper
def _suspend_self(namespace, suspended):
複製程式碼
這是個讓人有點害怕的函式名。暫停?這可不好,但我們是可以解決問題的。_suspend_self
函式是 functools.partial
的一個簡單應用,它的工作原理是:通過從外部函式作用域中捕獲 namespace
,並把它懸停在內部函式中。
def suspender(*args, **kwargs):
return suspended(namespace, *args, **kwargs)
複製程式碼
接下來,這個內部的函式呼叫了和第一個引數 namespace 一起傳遞進來的函式 suspended,實際上這是將方法又包了一層,這樣它就可以應用在一個普通的 Python 類上。_suspend_self
餘下的部分就只是設定一些屬性,這些屬性在某些時候可能會被對映(reflection)用到(我可能漏掉一些內容)。
猛獸(beast)
下一個函式是 make_class
。從它的簽名中我們能知道什麼?
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
namespace 是關鍵的部分,然而我還沒有解說。類中的每一個(或者絕大部分)方法都會將 self
作為第一個引數,這個 self
就是函式執行的時候類的例項。
一個類的例項實際上就是一個你可以用 .
符號而不是數字索引訪問其內容的字典。所以需要一個可以傳入我們期望的函式的物件來模仿這個字典。於是我們就說,這個例項是一個 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、前端、後端、區塊鏈、產品、設計、人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。