2011-03-04 41 views
26

我想在定義類時註冊一個類的實例。理想情況下,下面的代碼可以做到這一點。如何在定義類時自動註冊類

registry = {} 

def register(cls): 
    registry[cls.__name__] = cls() #problem here 
    return cls 

@register 
class MyClass(Base): 
    def __init__(self): 
     super(MyClass, self).__init__() 

不幸的是,這段代碼會生成錯誤NameError: global name 'MyClass' is not defined

發生了什麼是在#problem here線我試圖實例化MyClass但裝飾尚未返回,因此它不存在。

圍繞這個使用元類或其他什麼東西?

回答

31

是的,元類可以做到這一點。元類'__new__方法返回類,所以只需在返回它之前註冊該類。

class MetaClass(type): 
    def __new__(cls, clsname, bases, attrs): 
     newclass = super(MetaClass, cls).__new__(cls, clsname, bases, attrs) 
     register(newclass) # here is your register function 
     return newclass 

class MyClass(object): 
    __metaclass__ = MetaClass 

上一個例子在Python 2.x中有效。在Python 3.x中的MyClass的定義略有不同(雖然MetaClass沒有顯示,因爲它是不變的 - 除了super(MetaClass, cls)可以成爲super()如果你想):

#Python 3.x 

class MyClass(metaclass=MetaClass): 
    pass 

[編輯:固定失蹤cls參數super().__new__()]

[編輯:加入的Python 3.x的示例]

[編輯:校正參數傳遞給超級()的順序,以及改進的3.x的差異描述]

+3

順便說一下,這是一個真正的世界範例代碼,完全這(這不是我的代碼,但我已經使用了很多庫)。看看第67-90行(截至我寫這篇文章)。 https://github.com/ask/celery/blob/master/celery/task/base.py – dappawit 2011-03-04 03:20:46

+0

你救了我的命,謝謝你的簡單片段 – 2011-06-29 01:48:37

+0

@dappawit:芹菜似乎現在不使用這種技術。但是,它對我來說真的很好! – 2012-06-04 06:46:14

1

調用基類直接就可以(而不是使用超()):

def __init__(self): 
     Base.__init__(self) 
+0

我投了票,因爲這實際上是(種)一個正確的答案,並不值得3 downvotes。雖然沒有解釋,所以我不會投這個答案,如果它在0上。 – Ben 2012-06-08 03:31:44

+0

確實:Base .__ init __(self)是最簡單的解決方案。 – RicLeal 2015-11-04 15:36:52

11

的問題實際上不是由您指定的線路引起的,而是由__init__方法super調用。如果您使用dappawit建議的元類,問題仍然存在;來自該答案的示例的原因僅僅是,dappawit通過省略Base類以及因此調用super來簡化了您的示例。在以下示例中,既不ClassWithMeta也不DecoratedClass工作:

registry = {} 
def register(cls): 
    registry[cls.__name__] = cls() 
    return cls 

class MetaClass(type): 
    def __new__(cls, clsname, bases, attrs): 
     newclass = super(cls, MetaClass).__new__(cls, clsname, bases, attrs) 
     register(newclass) # here is your register function 
     return newclass 

class Base(object): 
    pass 


class ClassWithMeta(Base): 
    __metaclass__ = MetaClass 

    def __init__(self): 
     super(ClassWithMeta, self).__init__() 


@register 
class DecoratedClass(Base): 
    def __init__(self): 
     super(DecoratedClass, self).__init__() 

的問題是在兩種情況下是相同的;在類對象被創建之後,但在被綁定到名稱之前,調用register函數(通過元類或直接作爲修飾器)。這是super變得粗糙(在Python 2.x中),因爲它要求您參考super調用中的類,您只能通過使用全局名稱並相信它將被綁定到該名稱才合理地執行該調用直到調用super調用。在這種情況下,這種信任是錯位的。

我認爲這是一個元類錯誤的解決方案。元類用於構造族,它們有一些共同的自定義行爲,正如類用於使一系列具有一些自定義行爲的實例一樣。你所做的只是調用一個類的函數。你不會定義一個類來調用一個字符串的函數,你也不應該定義一個元類來調用一個類的函數。(1)在類創建過程中使用鉤子來創建類的實例,(2)使用super

解決此問題的一種方法是不使用supersuper解決了一個難題,但它引入了其他人(這是其中之一)。如果您使用複雜的多重繼承方案,super的問題比不使用super的問題更好,如果您從使用super的第三方類繼承,那麼您必須使用super。如果這兩個條件都不成立,那麼只需用直接基類調用替換super調用實際上可能是一個合理的解決方案。

另一種方法是不要將register掛鉤到類的創建中。在每個類定義之後添加register(MyClass)相當於在它們之前添加@register或在它們之前添加__metaclass__ = Registered(或任何您稱之爲元類的)。儘管如此,底部的線條比自我記錄要少得多,所以這並不好,但它實際上可能是一個合理的解決方案。

最後,你可以轉向不愉快的黑客,但可能會工作。問題在於,在之前,模塊的全局範圍中正在查找名稱。所以,你可以欺騙,如下:

def register(cls): 
    name = cls.__name__ 
    force_bound = False 
    if '__init__' in cls.__dict__: 
     cls.__init__.func_globals[name] = cls 
     force_bound = True 
    try: 
     registry[name] = cls() 
    finally: 
     if force_bound: 
      del cls.__init__.func_globals[name] 
    return cls 

這裏是如何工作的:

  1. 我們首先檢查__init__是否cls.__dict__(相對於它是否有一個__init__屬性,這將永遠是真實的)。如果它繼承了另一個類的__init__方法,我們可能沒問題(因爲超類已經按照通常的方式綁定到它的名字),而我們即將做的魔術在object.__init__上不起作用,所以我們想要避免嘗試,如果課程使用默認__init__
  2. 我們查找__init__方法,並抓住它的func_globals字典,這是全局查找(例如查找在super調用中引用的類)的字典。這通常是模塊的全局字典,其中最初定義了__init__方法。這樣的字典是cls.__name__插入它,只要register返回,所以我們只是自己插入它提前。
  3. 我們最終創建一個實例並將其插入到註冊表中。這是在try/finally塊中,以確保我們移除了我們創建的綁定,無論是否創建實例拋出異常;這是不太必要的(因爲99.999%的名字即將被反彈),但最好讓這樣的怪異魔法儘可能絕緣,以儘量減少某天其他怪異魔法與其他怪物互動的機率它。

這個版本的register無論是作爲裝飾者還是元類(我仍然認爲它不是元類的好用)都會起作用。有一些模糊的情況下,它會失敗,雖然:

  1. 我能想象一個奇怪的類__init__方法,但繼承了一個調用self.someMethod,並someMethod在類中重寫被定義和撥打電話super。可能不太可能。
  2. __init__方法最初可能已在另一個模塊中定義,然後在類塊中通過執行__init__ = externally_defined_function在類中使用。其他模塊的func_globals屬性儘管如此,這意味着我們的臨時綁定會在該模塊(oops)中打破該類名稱的任何定義。再次,不太可能。
  3. 可能是其他奇怪的情況,我沒有想到。

您可以嘗試添加更多黑客手段,使其在這些情況下更穩健一些,但是Python的本質是這兩種黑客都是可能的,並且不可能使它們絕對防彈。

相關問題