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.