constructores - decoradores python 3
¿Qué es una forma limpia y pitónica de tener varios constructores en Python? (11)
No puedo encontrar una respuesta definitiva para esto. AFAIK, no puedes tener múltiples funciones __init__
en una clase de Python. Entonces, ¿cómo resuelvo este problema?
Supongamos que tengo una clase llamada Cheese
con la propiedad number_of_holes
. ¿Cómo puedo tener dos formas de crear objetos de queso ...
- uno que tiene una serie de agujeros como este:
parmesan = Cheese(num_holes = 15)
- y uno que no toma argumentos y simplemente asigna al azar la propiedad
number_of_holes
:gouda = Cheese()
Puedo pensar en una sola manera de hacer esto, pero eso parece un poco torpe:
class Cheese():
def __init__(self, num_holes = 0):
if (num_holes == 0):
# randomize number_of_holes
else:
number_of_holes = num_holes
¿Qué dices? ¿Hay otra manera?
¿Por qué crees que tu solución es "torpe"? Personalmente, preferiría un constructor con valores predeterminados sobre varios constructores sobrecargados en situaciones como la suya (de todos modos Python no admite la sobrecarga de métodos):
def __init__(self, num_holes=None):
if num_holes is None:
# Construct a gouda
else:
# custom cheese
# common initialization
Para casos realmente complejos con muchos constructores diferentes, podría ser más limpio usar diferentes funciones de fábrica en su lugar:
@classmethod
def create_gouda(cls):
c = Cheese()
# ...
return c
@classmethod
def create_cheddar(cls):
# ...
En su ejemplo de queso, es posible que desee utilizar una subclase de queso Gouda ...
Así es como lo resolví para una clase de YearQuarter
que tuve que crear. __init__
un __init__
con un solo parámetro llamado value
. El código para __init__
simplemente decide qué tipo de parámetro de value
es y procesa los datos en consecuencia. En caso de que desee varios parámetros de entrada, simplemente los empaca en una sola tupla y prueba que el value
sea una tupla.
Lo usas así:
>>> temp = YearQuarter(datetime.date(2017, 1, 18))
>>> print temp
2017-Q1
>>> temp = YearQuarter((2017, 1))
>>> print temp
2017-Q1
Y así es como se ve __init__
y el resto de la clase:
import datetime
class YearQuarter:
def __init__(self, value):
if type(value) is datetime.date:
self._year = value.year
self._quarter = (value.month + 2) / 3
elif type(value) is tuple:
self._year = int(value[0])
self._quarter = int(value[1])
def __str__(self):
return ''{0}-Q{1}''.format(self._year, self._quarter)
Puede expandir el __init__
con varios mensajes de error, por supuesto. Los omití para este ejemplo.
Definitivamente, uno debería preferir las soluciones ya publicadas, pero como nadie mencionó esta solución todavía, creo que vale la pena mencionarlo para completar.
El enfoque de @classmethod
se puede modificar para proporcionar un constructor alternativo que no invoque al constructor predeterminado ( __init__
). En su lugar, se crea una instancia utilizando __new__
.
Esto podría usarse si el tipo de inicialización no se puede seleccionar en función del tipo del argumento del constructor, y los constructores no comparten el código.
Ejemplo:
class MyClass(set):
def __init__(self, filename):
self._value = load_from_file(filename)
@classmethod
def from_somewhere(cls, somename):
obj = cls.__new__(cls) # Does not call __init__
obj._value = load_from_somewhere(somename)
return obj
En realidad, None
es mucho mejor para los valores "mágicos":
class Cheese():
def __init__(self, num_holes = None):
if num_holes is None:
...
Ahora si quieres libertad total para agregar más parámetros:
class Cheese():
def __init__(self, *args, **kwargs):
#args -- tuple of anonymous arguments
#kwargs -- dictionary of named arguments
self.num_holes = kwargs.get(''num_holes'',random_holes())
Para explicar mejor el concepto de *args
y **kwargs
(puedes cambiar estos nombres):
def f(*args, **kwargs):
print ''args: '', args, '' kwargs: '', kwargs
>>> f(''a'')
args: (''a'',) kwargs: {}
>>> f(ar=''a'')
args: () kwargs: {''ar'': ''a''}
>>> f(1,2,param=3)
args: (1, 2) kwargs: {''param'': 3}
Esas son buenas ideas para su implementación, pero si está presentando una interfaz para hacer queso a un usuario. No les importa cuántos agujeros tiene el queso o qué partes internas se utilizan para hacer queso. El usuario de su código solo quiere "gouda" o "parmesean", ¿verdad?
Entonces, ¿por qué no hacer esto?
# cheese_user.py
from cheeses import make_gouda, make_parmesean
gouda = make_gouda()
paremesean = make_parmesean()
Y luego puede usar cualquiera de los métodos anteriores para implementar las funciones:
# cheeses.py
class Cheese(object):
def __init__(self, *args, **kwargs):
#args -- tuple of anonymous arguments
#kwargs -- dictionary of named arguments
self.num_holes = kwargs.get(''num_holes'',random_holes())
def make_gouda():
return Cheese()
def make_paremesean():
return Cheese(num_holes=15)
Esta es una buena técnica de encapsulación, y creo que es más Pythonic. Para mí, esta forma de hacer las cosas encaja más en línea con la escritura de pato. Simplemente estás pidiendo un objeto Gouda y no te importa qué clase es.
La mejor respuesta es la que aparece arriba sobre los argumentos predeterminados, pero me divertí escribiendo esto, y ciertamente encaja en la factura de "constructores múltiples". Úselo bajo su propio riesgo.
¿Qué pasa con el new método.
"Las implementaciones típicas crean una nueva instancia de la clase invocando el método new () de la superclase usando super (currentclass, cls). New (cls [, ...]) con los argumentos apropiados y luego modificando la instancia recién creada según sea necesario antes devolviéndolo ".
Por lo tanto, puede hacer que el nuevo método modifique la definición de su clase adjuntando el método constructor apropiado.
class Cheese(object):
def __new__(cls, *args, **kwargs):
obj = super(Cheese, cls).__new__(cls)
num_holes = kwargs.get(''num_holes'', random_holes())
if num_holes == 0:
cls.__init__ = cls.foomethod
else:
cls.__init__ = cls.barmethod
return obj
def foomethod(self, *args, **kwargs):
print "foomethod called as __init__ for Cheese"
def barmethod(self, *args, **kwargs):
print "barmethod called as __init__ for Cheese"
if __name__ == "__main__":
parm = Cheese(num_holes=5)
Todas estas respuestas son excelentes si desea usar parámetros opcionales, pero otra posibilidad de Pythonic es usar un método de clase para generar un pseudo-constructor de estilo de fábrica:
def __init__(self, num_holes):
# do stuff with the number
@classmethod
def fromRandom(cls):
return cls( # some-random-number )
Usar num_holes=None
como predeterminado está bien si va a tener solo __init__
.
Si desea "constructores" múltiples e independientes, puede proporcionarlos como métodos de clase. Estos son generalmente llamados métodos de fábrica. En este caso, podría tener el valor predeterminado para num_holes
ser 0
.
class Cheese(object):
def __init__(self, num_holes=0):
"defaults to a solid cheese"
self.number_of_holes = num_holes
@classmethod
def random(cls):
return cls(randint(0, 100))
@classmethod
def slightly_holey(cls):
return cls(randint((0,33))
@classmethod
def very_holey(cls):
return cls(randint(66, 100))
Ahora crea un objeto como este:
gouda = Cheese()
emmentaler = Cheese.random()
leerdammer = Cheese.slightly_holey()
Use num_holes=None
como predeterminado, en su lugar. Luego verifique si num_holes is None
, y si es así, seleccione aleatoriamente. Eso es lo que generalmente veo, de todos modos.
Los métodos de construcción más radicalmente diferentes pueden justificar un método de clase que devuelve una instancia de cls
.
Yo usaría la herencia. Especialmente si va a haber más diferencias que número de agujeros. Especialmente si Gouda necesitará tener un conjunto diferente de miembros, entonces Parmesan.
class Gouda(Cheese):
def __init__(self):
super(Gouda).__init__(num_holes=10)
class Parmesan(Cheese):
def __init__(self):
super(Parmesan).__init__(num_holes=15)
class Cheese:
def __init__(self, *args, **kwargs):
"""A user-friendly initialiser for the general-purpose constructor.
"""
...
def _init_parmesan(self, *args, **kwargs):
"""A special initialiser for Parmesan cheese.
"""
...
def _init_gauda(self, *args, **kwargs):
"""A special initialiser for Gauda cheese.
"""
...
@classmethod
def make_parmesan(cls, *args, **kwargs):
new = cls.__new__(cls)
new._init_parmesan(*args, **kwargs)
return new
@classmethod
def make_gauda(cls, *args, **kwargs):
new = cls.__new__(cls)
new._init_gauda(*args, **kwargs)
return new