2011-05-10 86 views
30

我在Python中遇到了一個奇怪的錯誤,其中使用類的__new__方法作爲工廠將導致實例化類的方法被調用兩次。使用類'__new__方法作爲工廠:__init__被調用兩次

這個想法最初是使用母類的__new__方法根據傳遞的參數返回她的一個孩子的特定實例,而無需在類之外聲明工廠函數。

我知道使用工廠函數將是這裏使用的最佳設計模式,但在項目的這一點改變設計模式將代價高昂。因此,我的問題是:有沒有辦法避免對__init__的雙重打電話,並且在這樣的模式下只能撥打__init__

class Shape(object): 
    def __new__(cls, desc): 
     if cls is Shape: 
      if desc == 'big': return Rectangle(desc) 
      if desc == 'small': return Triangle(desc) 
     else: 
      return super(Shape, cls).__new__(cls, desc) 

    def __init__(self, desc): 
     print "init called" 
     self.desc = desc 

class Triangle(Shape): 
    @property 
    def number_of_edges(self): return 3 

class Rectangle(Shape): 
    @property 
    def number_of_edges(self): return 4 

instance = Shape('small') 
print instance.number_of_edges 

>>> init called 
>>> init called 
>>> 3 

任何幫助非常感謝。

回答

41

構建對象時,Python會調用它的__new__方法來創建對象,然後在返回的對象上調用__init__。當您從__new__內部通過調用Triangle()創建對象時,將導致進一步調用__new____init__

你應該做的是:

class Shape(object): 
    def __new__(cls, desc): 
     if cls is Shape: 
      if desc == 'big': return super(Shape, cls).__new__(Rectangle) 
      if desc == 'small': return super(Shape, cls).__new__(Triangle) 
     else: 
      return super(Shape, cls).__new__(cls, desc) 

,這將創造一個RectangleTriangle而不會觸發到__init__打個電話,然後__init__只調用一次。

編輯回答@阿德里安的問題,如何超工作:

super(Shape,cls)搜索cls.__mro__找到Shape,然後向下搜索序列的其餘找到屬性。

Triangle.__mro__(Triangle, Shape, object)Rectangle.__mro__(Rectangle, Shape, object)Shape.__mro__只是(Shape, object)。 對於任何的那些情況下,當你調用super(Shape, cls)它忽略了MRO SQUENCE一切直至幷包括Shape所以唯一剩下的就是單一元素的元組(object,)以及用於查找所需的屬性。

這將變得更加複雜,如果你有一個菱形繼承:

class A(object): pass 
class B(A): pass 
class C(A): pass 
class D(B,C): pass 

現在B中的方法可以使用super(B, cls),如果它是A B實例將搜索(A, object),但如果你有一個D實例相同調用B將搜索(C, A, object)因爲D.__mro__(B, C, A, object)

因此,在這種特殊情況下,你可以定義修改形狀的建設行爲的新的混合類,你可以有專門的三角形和矩形從現有的繼承,但構造不同。

+0

非常感謝,這完美地解決了我的問題。 – xApple

+1

使用'return Rectangle .__ new __(Rectangle)'會不會更好,因爲這樣可以保證如果定義了'Rectangle'的'__new__'會被調用嗎? –

+2

@Georg,如果你這樣做,你將必須非常小心,以避免無限遞歸。任何類特定的初始化應該放在'__init__'中,所以我認爲在這裏假設'__new__'唯一的工作是創建一個正確類型的對象是非常安全的。 – Duncan

0

我實際上無法在我安裝的任何一個Python解釋器中重現這種行爲,所以這是一種猜測。但是...

__init__被調用兩次,因爲您正在初始化兩個對象:原始對象Shape,然後是其子對象之一。如果您更改__init__以便它也打印正在初始化的對象的類,您將會看到這一點。

print type(self), "init called" 

這是無害的,因爲原來的Shape將被丟棄,因爲你是不是在你的__new__()返回對它的引用。

由於調用一個函數是在語法上相同的來實例化一個類,你可以改變它爲一個函數而不會改變任何東西,我建議你這樣做。我不明白你的不情願。

+1

這是不正確的 - 第一個'__init__'調用發生* *內外部'__new__'調用(當'三角()'和'矩形()'的稱呼),但那麼,由於'__new__'返回了'Shape'的一個實例,原始的'Shape()'調用再次在已經初始化的對象上調用'__init__' * *。注意,如果'__new __()'返回的對象不是'Shape()'的一個實例,那麼'__init__'不會被調用(如果類層次結構不是對)。 – ncoghlan

+0

事實上,兩者在語法上都是相同的。我的不情願源於這樣一個事實,即如果我定義了一個名爲「Shape」的函數,我必須將我的類重命名爲「_Shape」。這當然會導致一些變量重命名,但大多數情況下它會對sphinx-autodoc生成的其他文件造成複雜的後果。 – xApple

+0

我想你確實想公開這些類的文檔,所以你不能只聲明它們在*函數中。你可以嘗試在'__init __()'中重新分配實例的'__class__'屬性,而不是'__new __()'。 – kindall

10

發佈我的問題後,我繼續尋找解決的找到了一種方法來解決,看起來像一個黑客位的問題。這不如鄧肯的解決方案,但我認爲可以有趣的是不要少說。該Shape類變爲:

class ShapeFactory(type): 
    def __call__(cls, desc): 
     if cls is Shape: 
      if desc == 'big': return Rectangle(desc) 
      if desc == 'small': return Triangle(desc) 
     return type.__call__(cls, desc) 

class Shape(object): 
    __metaclass__ = ShapeFactory 
    def __init__(self, desc): 
     print "init called" 
     self.desc = desc 
+0

你爲什麼說這不如鄧肯的解決方案。這似乎更清楚發生了什麼事情,我更不hacky。也是元。 –

+1

我認爲這不太明顯,因爲它在程序中增加了第四個類,並且涉及不是每個人都熟悉的'__metaclass__'黑魔法。 – xApple