[譯] 不用 Class,如何寫一個類

不用 Class,如何寫一個類

前言

Python 的對象模型使人難以置信的強大;實際上,你能夠重寫全部(對象),或者向任何人分發奇怪的對象,並讓他們像對待正常的對象的那樣接受它。前端

Python 的面向對象是 smalltalk 面向對象的一個後裔。在 Python 中,一切都是對象,甚至對象集和對象類型都是如此;特別的,函數也是對象。這讓我很好奇:不使用類建立一個類是否可能?android

代碼

這個想法的關鍵性代碼以下所示。這是一個很基礎的實現,但它支持 __call__ 這樣的邊緣狀況(但不支持其餘魔術方法,由於他們須要加載依賴)。後文將會解說。ios

有沒有搞錯?

這是一些很先進的 Python 輪子,它用一種和對象的設計初衷毫不相同的方法使用了一些對象。咱們將分段解說代碼。git

第一個 helper

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)用到(我可能漏掉一些內容)。後端

猛獸(beast)

下一個函數是 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

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 函數返回另外一個函數,調用這個函數就能獲取到實例並完成它的初始化。

如今咱們就獲得它了,一個徹底沒用類的類。打賭你會實際應用它。


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索