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