Использование метода класса __new__ в качестве Factory: __init__ вызывается дважды

Я столкнулся с странной ошибкой в ​​python, где использование метода __new__ класса как factory приведет к вызову метода __init__ экземпляра класса, который будет вызываться дважды.

Первоначально идея заключалась в том, что метод __new__ материнского класса возвращал конкретный экземпляр одного из ее детей в зависимости от переданных параметров без необходимости объявлять функцию factory вне класса.

Я знаю, что использование функции factory было бы лучшим шаблоном проектирования, который будет использоваться здесь, но изменение шаблона проектирования в этой точке проекта было бы дорогостоящим. Поэтому мой вопрос: есть ли способ избежать двойного вызова __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

Любая помощь очень ценится.

Ответ 1

Когда вы создаете объект, 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)

который создаст Rectangle или Triangle, не вызывая вызов __init__, а затем __init__ вызывается только один раз.

Изменить для ответа на вопрос @Adrian о том, как работает super:

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, поэтому остается только один элемент tuple (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__ > is (B, C, A, object).

Итак, в этом конкретном случае вы можете определить новый класс mixin, который изменяет поведение конструкции фигур, и вы могли бы иметь специализированные треугольники и прямоугольники, наследуемые от существующих, но построенные по-разному.

Ответ 2

После публикации моего вопроса я продолжил поиск решения и нашел способ решить проблему, которая выглядит как взломанный. Он уступает решению Дункана, но я думал, что было бы интересно отметить, тем не менее. Класс 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

Ответ 3

Я не могу воспроизвести это поведение ни в одном из интерпретаторов Python, которые я установил, так что это что-то вроде догадки. Однако...

__init__ вызывается дважды, потому что вы инициализируете два объекта: исходный объект Shape, а затем один из его подклассов. Если вы измените свой __init__, чтобы он также распечатывал класс инициализируемого объекта, вы увидите это.

print type(self), "init called"

Это безобидно, потому что исходный Shape будет отброшен, так как вы не возвращаете ссылку на него в __new__().

Так как вызов функции синтаксически идентичен созданию экземпляра класса, вы можете изменить его на функцию без изменения чего-либо еще, и я рекомендую вам сделать именно это. Я не понимаю вашего нежелания.