¿Qué es una forma limpia y pitónica de tener múltiples constructores en Python?


No puedo encontrar una respuesta definitiva para esto. AFAIK, no puedes tener múltiples funciones __init__ en una clase 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?..

  1. uno que toma un número de agujeros como este: parmesan = Cheese(num_holes = 15)
  2. y uno que no toma argumentos y simplemente aleatoriza la propiedad number_of_holes: gouda = Cheese()

Solo puedo pensar en una 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?

Author: smci, 2009-03-25

11 answers

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 completa libertad de añadir 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 (en realidad puede 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}

Http://docs.python.org/reference/expressions.html#calls

 693
Author: vartec,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-03-07 00:41:58

Usar num_holes=None como predeterminado está bien si vas a tener solo __init__.

Si desea varios "constructores" independientes, puede proporcionarlos como métodos de clase. Estos generalmente se llaman métodos de fábrica. En este caso, el valor predeterminado para num_holes es 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()
 532
Author: Ber,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-03-01 15:29:51

Todas estas respuestas son excelentes si desea usar parámetros opcionales, pero otra posibilidad pitónica es usar un método de clase para generar un pseudo-constructor estilo fábrica:

def __init__(self, num_holes):

  # do stuff with the number

@classmethod
def fromRandom(cls):

  return cls( # some-random-number )
 19
Author: Yes - that Jake.,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-03-25 17:16:04

¿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 (Python no admite la sobrecarga de métodos de todos modos):

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 cheese, es posible que desee usar una subclase Gouda de Cheese...

 18
Author: Ferdinand Beyer,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-03-25 17:11:35

Esas son buenas ideas para su implementación, pero si está presentando una interfaz de fabricación de queso a un usuario. No les importa cuántos agujeros tiene el queso o qué componentes internos entran en la fabricación de queso. El usuario de tu 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 realmente 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 la encapsulación, y creo que es más Python. A mí esta forma de hacer las cosas encaja más en línea más con la tipificación de pato. Simplemente estás pidiendo un objeto gouda y realmente no te importa qué clase es.

 17
Author: Brad C,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-10-25 15:06:03

La mejor respuesta es la anterior sobre los argumentos predeterminados, pero me divertí escribiendo esto, y ciertamente se ajusta a la factura de "constructores múltiples". Utilizar bajo su propio riesgo.

¿Qué pasa con el nuevo método.

"Implementaciones típicas crean una nueva instancia de la clase invocando el método new () de la superclase usando super(currentclass, cls). nuevo (cls [,...]) con los argumentos apropiados y luego modificar la instancia recién creada como necesario antes de devolverlo."

Así que puedes hacer que el método new modifique la definición de tu 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)
 10
Author: mluebke,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-03-25 19:55:04

Uno definitivamente debería preferir las soluciones ya publicadas, pero ya que nadie mencionó esta solución todavía, creo que vale la pena mencionarla para completar.

El enfoque @classmethod se puede modificar para proporcionar un constructor alternativo que no invoque al constructor predeterminado (__init__). En su lugar, se crea una instancia usando __new__.

Esto se puede usar si el tipo de inicialización no se puede seleccionar en función del tipo del argumento constructor, y los constructores lo hacen no compartir 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
 10
Author: Andrzej Pronobis,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-08-11 00:20:55

Use num_holes=None como predeterminado, en su lugar. A continuación, compruebe si num_holes is None, y si es así, aleatorizar. Eso es lo que generalmente veo, de todos modos.

Métodos de construcción más radicalmente diferentes pueden justificar un método de clases que devuelve una instancia de cls.

 8
Author: Devin Jeanpierre,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-03-25 17:03:56

Usaría herencia. Especialmente si va a haber más diferencias que el número de agujeros. Especialmente si Gouda necesitará tener diverso sistema de miembros entonces Parmesano.

class Gouda(Cheese):
    def __init__(self):
        super(Gouda).__init__(num_holes=10)


class Parmesan(Cheese):
    def __init__(self):
        super(Parmesan).__init__(num_holes=15) 
 2
Author: Michel Samia,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-04-28 12:33:56

Así es como lo resolví para una clase YearQuarter que tuve que crear. He creado un __init__ con un único parámetro llamado value. El código para el __init__ solo decide qué tipo es el parámetro value y procesa los datos en consecuencia. En caso de que desee múltiples parámetros de entrada, simplemente los empaqueta en una sola tupla y pruebe que value es 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 ven __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 ampliar el __init__ con múltiples mensajes de error, por supuesto. Los omití para este ejemplo.

 0
Author: Elmex80s,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-01-18 12:55:35
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
 0
Author: Alexey,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-02-25 21:52:40