web-dev-qa-db-ja.com

クラスの__new__メソッドをファクトリとして使用する:__ init__は2回呼び出されます

pythonで、クラスの__new__メソッドをファクトリとして使用すると、インスタンス化されたクラスの__init__メソッドが2回呼び出されるという奇妙なバグが発生しました。

当初のアイデアは、母クラスの__new__メソッドを使用して、クラスの外部でファクトリ関数を宣言することなく、渡されたパラメータに応じて子の1つの特定のインスタンスを返すことでした。

ここで使用するのにファクトリ関数を使用するのが最適なデザインパターンであることはわかっていますが、プロジェクトのこの時点でデザインパターンを変更するとコストがかかります。したがって、私の質問は、__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

どんな助けでも大歓迎です。

38
xApple

オブジェクトを作成するとき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)

これにより、__init__の呼び出しをトリガーせずに、RectangleまたはTriangleが作成され、__init__が1回だけ呼び出されます。

スーパーがどのように機能するかについての@Adrianの質問に答えるために編集してください:

super(Shape,cls)cls.__mro__を検索してShapeを見つけ、次にシーケンスの残りを検索して属性を見つけます。

Triangle.__mro__(Triangle, Shape, object)であり、Rectangle.__mro__(Rectangle, Shape, object)であり、Shape.__mro__は単なる(Shape, object)です。 super(Shape, cls)を呼び出すと、Shapeまでのmroシーケンス内のすべてが無視されるため、残っているのは単一要素のタプル(object,)だけであり、これを使用して必要な属性。

ダイヤモンドの継承がある場合、これはより複雑になります。

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

これで、Bのメソッドはsuper(B, cls)を使用し、それがBインスタンスの場合は(A, object)を検索しますが、Dインスタンスがある場合は、Bの同じ呼び出しで(C, A, object)を検索します。 D.__mro__(B, C, A, object)です。

したがって、この特定のケースでは、シェイプの構築動作を変更する新しいミックスインクラスを定義し、既存のものを継承しているが異なる方法で構築された特殊な三角形と長方形を作成できます。

59
Duncan

質問を投稿した後、私は解決策を探し続け、ちょっとしたハックのように見える問題を解決する方法を見つけました。ダンカンのソリューションより劣っていますが、それでも言及するのは面白いかもしれないと思いました。 Shapeclassは次のようになります。

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
12
xApple

インストールしたPythonインタープリターのどちらでも、この動作を実際に再現することはできないので、これは推測のようなものです。しかし...

___init___は、元のShapeオブジェクトと、そのサブクラスの1つという2つのオブジェクトを初期化しているため、2回呼び出されています。 ___init___を変更して、初期化されているオブジェクトのクラスも出力するようにすると、これが表示されます。

_print type(self), "init called"
_

元のShapeは、__new__()で参照を返さないため、破棄されるため、これは無害です。

関数の呼び出しは、クラスのインスタンス化と構文的に同一であるため、他に何も変更せずにこれを関数に変更できます。正確に行うことをお勧めします。私はあなたの不本意を理解していません。

0
kindall