Pregunta ¿Cuál 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 __init__ funciones en una clase de Python. Entonces, ¿cómo soluciono este problema?

Supongamos que tengo una clase llamada Cheese con el number_of_holes propiedad. ¿Cómo puedo tener dos formas de crear objetos de queso ...

  1. uno que toma varios agujeros como este: parmesan = Cheese(num_holes = 15)
  2. y uno que no toma argumentos y solo aleatoriza el number_of_holes propiedad: gouda = Cheese()

Solo puedo pensar en una forma de hacerlo, pero 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?


546
2018-03-25 17:00


origen


Respuestas:


Actualmente None es mucho mejor para los valores "mágicos":

class Cheese():
    def __init__(self, num_holes = None):
        if num_holes is None:
            ...

Ahora, si quiere total libertad 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 (en realidad 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}

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


681
2018-03-25 17:03



Utilizando num_holes=None como el valor predeterminado está bien si vas a tener solo __init__.

Si quiere "constructores" múltiples e independientes, puede proporcionarlos como métodos de clase. Estos generalmente se llaman 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()

522
2018-03-25 17:11



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 pseudoconstructor de fábrica:

def __init__(self, num_holes):

  # do stuff with the number

@classmethod
def fromRandom(cls):

  return cls( # some-random-number )

19
2018-03-25 17:16



¿Por qué crees que tu solución es "torpe"? Personalmente, preferiría un constructor con valores predeterminados sobre múltiples 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 queso, es posible que desee utilizar una subclase Gouda de Queso aunque ...


18
2018-03-25 17:11



Esas son buenas ideas para su implementación, pero si está presentando una interfaz para hacer quesos a un usuario. No les importa cuántos agujeros tiene el queso o qué componentes internos entran en la fabricación del queso. El usuario de tu código solo quiere "gouda" o "parmesean" ¿no?

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 encapsulación, y creo que es más Pythonic. Para mí, esta forma de hacer las cosas se ajusta más a la tipificación del pato. Simplemente estás pidiendo un objeto Gouda y realmente no te importa qué clase es.


17
2017-10-25 15:06



La mejor respuesta es la anterior acerca de los argumentos predeterminados, pero me divertí escribiendo esto, y ciertamente encaja en la factura de "múltiples constructores". Úselo bajo su propio riesgo.

Qué pasa con la nuevo método.

"Las implementaciones típicas crean una nueva instancia de la clase invocando la superclase nuevo() método que usa super (currentclass, cls).nuevo(cls [, ...]) con argumentos apropiados y luego modificando la instancia recién creada según sea necesario antes de devolverla. "

Entonces puedes tener el nuevo método modifique su definición de clase adjuntando el método de 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
2018-03-25 19:48



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

los @classmethod enfoque puede ser modificado para proporcionar un constructor alternativo que no invoque el constructor predeterminado (__init__) En cambio, se crea una instancia usando __new__.

Esto podría usarse si el tipo de inicialización no se puede seleccionar en función del tipo de 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

10
2017-08-11 00:20



Utilizar num_holes=None como predeterminado, en cambio. Luego verifique si num_holes is None, y si es así, aleatorice. Eso es lo que generalmente veo, de todos modos.

Los métodos de construcción más radicalmente diferentes pueden garantizar un método de clase que devuelva una instancia de cls.


8
2018-03-25 17:03