metodos español __new__ __init__ python design-patterns inheritance class-design

python - español - Usando un método de clase ''__new__'' como fábrica:__init__ se llama dos veces



__new__ python (3)

Me encontré con un extraño error en Python donde usar el método __new__ de una clase como una fábrica llevaría al método __init__ de la clase instanciada a ser llamado dos veces.

Originalmente, la idea era utilizar el método __new__ de la clase madre para devolver una instancia específica de uno de sus hijos, según los parámetros que se pasen, sin tener que declarar una función de fábrica fuera de la clase.

Sé que usar una función de fábrica sería el mejor patrón de diseño para usar aquí, pero cambiar el patrón de diseño en este punto del proyecto sería costoso. Mi pregunta es: ¿hay alguna manera de evitar la doble invocación a __init__ y obtener solo una sola llamada a __init__ en dicho esquema?

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

Cualquier ayuda muy apreciada.


Cuando construyes un objeto, Python llama a su método __new__ para crear el objeto y luego llama a __init__ sobre el objeto que se devuelve. Cuando crea el objeto desde adentro __new__ llamando a Triangle() generarán más llamadas a __new__ y __init__ .

Lo que debes hacer es:

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)

que creará un Rectangle o Triangle sin activar una llamada a __init__ y luego __init__ se llama solo una vez.

Edite para responder a la pregunta de Adrian sobre cómo funciona súper:

super(Shape,cls) busca cls.__mro__ para encontrar Shape y luego busca el resto de la secuencia para encontrar el atributo.

Triangle.__mro__ es (Triangle, Shape, object) y Rectangle.__mro__ is (Rectangle, Shape, object) mientras que Shape.__mro__ is just (Shape, object) . Para cualquiera de esos casos cuando llamas a super(Shape, cls) , ignora todo en la secuencia de mro hasta e incluyendo Shape por lo que lo único que queda es la tupla de un elemento (object,) y se usa para encontrar el atributo deseado.

Esto se volvería más complicado si tuvieras una herencia de diamantes:

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

ahora un método en B podría usar super(B, cls) y si fuera una instancia B buscaría (A, object) pero si tuviera una instancia D la misma llamada en B buscaría (C, A, object) porque el D.__mro__ es (B, C, A, object) .

Entonces, en este caso particular, podría definir una nueva clase mixin que modifique el comportamiento de construcción de las formas y podría tener triángulos y rectángulos especializados heredando de los existentes pero construidos de manera diferente.


Después de publicar mi pregunta, continué buscando una solución y encontré la manera de resolver el problema que parece un poco complicado. Es inferior a la solución de Duncan, pero pensé que podría ser interesante mencionar de todos modos. La clase Shape convierte en:

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


Realmente no puedo reproducir este comportamiento en ninguno de los intérpretes de Python que tengo instalados, así que esto es una especie de suposición. Sin embargo...

__init__ se __init__ dos veces porque está inicializando dos objetos: el objeto Shape original y luego una de sus subclases. Si cambias tu __init__ por lo que también imprime la clase del objeto que se está inicializando, verás esto.

print type(self), "init called"

Esto es inofensivo porque se descartará la Shape original, ya que no devolverá una referencia a ella en su __new__() .

Como llamar a una función es sintácticamente idéntico a crear una instancia de una clase, puede cambiar esto a una función sin cambiar nada más, y le recomiendo que haga exactamente eso. No entiendo tu reticencia.