Question Qu'est-ce qu'une façon propre et pythonique d'avoir plusieurs constructeurs en Python?


Je ne peux pas trouver de réponse définitive à cela. AFAIK, vous ne pouvez pas avoir plusieurs __init__ fonctionne dans une classe Python. Alors, comment puis-je résoudre ce problème?

Supposons que j'ai une classe appelée Cheese avec le number_of_holes propriété. Comment puis-je avoir deux façons de créer des objets-fromage ...

  1. celui qui prend un certain nombre de trous comme celui-ci: parmesan = Cheese(num_holes = 15)
  2. et celui qui ne prend pas d'arguments et ne fait que randomiser le number_of_holes propriété: gouda = Cheese()

Je ne peux penser qu'à un seul moyen de le faire, mais cela me semble un peu maladroit:

class Cheese():
    def __init__(self, num_holes = 0):
        if (num_holes == 0):
            # randomize number_of_holes
        else:
            number_of_holes = num_holes

Que dis-tu? Y a-t-il un autre moyen?


546
2018-03-25 17:00


origine


Réponses:


Réellement None est beaucoup mieux pour les valeurs "magiques":

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

Maintenant, si vous voulez une liberté totale d'ajouter plus de paramètres:

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())

Pour mieux expliquer le concept de *args et **kwargs (Vous pouvez réellement changer ces noms):

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



En utilisant num_holes=None comme la valeur par défaut est bien si vous allez avoir juste __init__.

Si vous voulez plusieurs "constructeurs" indépendants, vous pouvez les fournir en tant que méthodes de classe. Ils sont généralement appelés méthodes d'usine. Dans ce cas, vous pourriez avoir la valeur par défaut pour num_holes être 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))

Maintenant, créez un objet comme celui-ci:

gouda = Cheese()
emmentaler = Cheese.random()
leerdammer = Cheese.slightly_holey()

522
2018-03-25 17:11



Toutes ces réponses sont excellentes si vous souhaitez utiliser des paramètres facultatifs, mais une autre possibilité Pythonic consiste à utiliser une méthode de classe pour générer un pseudo-constructeur de style usine:

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



Pourquoi pensez-vous que votre solution est "maladroite"? Personnellement, je préférerais un constructeur avec des valeurs par défaut sur plusieurs constructeurs surchargés dans des situations comme la vôtre (Python ne supporte pas la surcharge de méthode de toute façon):

def __init__(self, num_holes=None):
    if num_holes is None:
        # Construct a gouda
    else:
        # custom cheese
    # common initialization

Pour les cas vraiment complexes avec beaucoup de constructeurs différents, il peut être plus propre d'utiliser différentes fonctions d'usine à la place:

@classmethod
def create_gouda(cls):
    c = Cheese()
    # ...
    return c

@classmethod
def create_cheddar(cls):
    # ...

Dans votre exemple de fromage, vous voudrez peut-être utiliser une sous-classe de fromage Gouda ...


18
2018-03-25 17:11



Ce sont de bonnes idées pour votre mise en œuvre, mais si vous présentez une interface de fabrication de fromage à un utilisateur. Ils ne se soucient pas du nombre de trous que le fromage a ou ce que les internes entrent dans la fabrication du fromage. L'utilisateur de votre code veut juste "gouda" ou "parmesean" à droite?

Alors pourquoi ne pas faire ceci:

# cheese_user.py
from cheeses import make_gouda, make_parmesean

gouda = make_gouda()
paremesean = make_parmesean()

Et puis vous pouvez utiliser l'une des méthodes ci-dessus pour implémenter réellement les fonctions:

# 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)

C'est une bonne technique d'encapsulation, et je pense que c'est plus Pythonic. Pour moi, cette façon de faire correspond davantage au typage du canard. Vous demandez simplement un objet gouda et vous ne vous souciez pas vraiment de quelle classe il s'agit.


17
2017-10-25 15:06



La meilleure réponse est celle ci-dessus à propos des arguments par défaut, mais je me suis amusé à écrire ceci, et cela correspond certainement à la facture pour "plusieurs constructeurs". À utiliser à vos risques et périls.

Que dire de la Nouveau méthode.

"Les implémentations typiques créent une nouvelle instance de la classe en invoquant la superclasse Nouveau() méthode en utilisant super (classe courante, cls).Nouveau(cls [, ...]) avec les arguments appropriés, puis en modifiant l'instance nouvellement créée si nécessaire avant de la renvoyer. "

Donc, vous pouvez avoir le Nouveau méthode modifier votre définition de classe en attachant la méthode constructeur appropriée.

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



On devrait définitivement préférer les solutions déjà postées, mais puisque personne n'a encore mentionné cette solution, je pense qu'il vaut la peine de mentionner pour l'exhaustivité.

le @classmethod L'approche peut être modifiée pour fournir un constructeur alternatif qui n'invoque pas le constructeur par défaut (__init__). Au lieu de cela, une instance est créée en utilisant __new__.

Cela peut être utilisé si le type d'initialisation ne peut pas être sélectionné en fonction du type de l'argument constructeur et que les constructeurs ne partagent pas le code.

Exemple:

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



Utilisation num_holes=None par défaut, à la place. Ensuite, vérifiez si num_holes is None, et si oui, randomiser. C'est ce que je vois généralement, de toute façon.

Des méthodes de construction plus radicalement différentes peuvent justifier une méthode de classe qui retourne une instance de cls.


8
2018-03-25 17:03



J'utiliserais l'héritage. Surtout s'il y aura plus de différences que de nombre de trous. Surtout si Gouda devra avoir un ensemble de membres différent de 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) 

2
2018-04-28 12:33



Voilà comment je l'ai résolu pour un YearQuarter classe que je devais créer. J'ai créé un __init__ avec un seul paramètre appelé value. Le code pour le __init__ décide juste quel type le value paramètre est et traite les données en conséquence. Dans le cas où vous voulez plusieurs paramètres d'entrée, il vous suffit de les placer dans un seul tuple et de tester value être un tuple.

Vous l'utilisez comme ceci:

>>> temp = YearQuarter(datetime.date(2017, 1, 18))
>>> print temp
2017-Q1
>>> temp = YearQuarter((2017, 1))
>>> print temp
2017-Q1

Et voici comment le __init__ et le reste de la classe ressemble à:

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)

Vous pouvez étendre le __init__ avec plusieurs messages d'erreur bien sûr. Je les ai omis pour cet exemple.


0
2018-01-18 12:17



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
2018-02-25 21:52