Используя метод класса __new__ в качестве Фабрики: __init__ вызывается дважды - PullRequest
35 голосов
/ 10 мая 2011

Я обнаружил странную ошибку в python, когда использование метода __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

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

Ответы [ 3 ]

54 голосов
/ 10 мая 2011

Когда вы создаете объект, 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__ вызывается только один раз.

Изменить, чтобы ответить на вопрос Адриана о том, как супер работает:

super(Shape,cls) выполняет поиск cls.__mro__, чтобы найти Shape, а затем выполняет поиск по оставшейся части последовательности, чтобы найти атрибут.

Triangle.__mro__ составляет (Triangle, Shape, object) и Rectangle.__mro__ равно (Rectangle, Shape, object), в то время как Shape.__mro__ равно (Shape, object). Для любого из тех случаев, когда вы вызываете super(Shape, cls), он игнорирует все в последовательности mro вплоть до Shape, поэтому остается только один элемент кортежа (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, который изменяет поведение построения фигур, и вы можете иметь специализированные треугольники и прямоугольники, наследующие от существующих, но построенные по-другому.

11 голосов
/ 11 мая 2011

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

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

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

print type(self), "init called"

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

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

...