web-dev-qa-db-fra.com

Qu'est-ce qu'une méthode pythonique propre pour avoir plusieurs constructeurs en Python?

Je ne trouve pas de réponse définitive à cela. Autant que je sache, vous ne pouvez pas avoir plusieurs fonctions __init__ dans une classe Python. Alors, comment puis-je résoudre ce problème? 

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

  1. un qui prend un certain nombre de trous comme celui-ci: parmesan = Cheese(num_holes = 15)
  2. et un qui ne prend aucun argument et ne fait que rendre aléatoire la propriété number_of_holes: gouda = Cheese()

Je ne peux penser qu'à une seule façon de faire cela, mais cela 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?

594
winsmith

En fait, None est bien meilleur pour les valeurs "magiques":

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

Maintenant, si vous voulez la liberté complète 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

726
vartec

Utiliser num_holes=None par défaut est acceptable si vous n’avez que __init__.

Si vous voulez plusieurs "constructeurs" indépendants, vous pouvez les fournir en tant que méthodes de classe. Celles-ci sont généralement appelées méthodes d'usine. Dans ce cas, la valeur par défaut pour num_holes serait 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 ceci:

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

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 )
21
Yes - that Jake.

Pourquoi pensez-vous que votre solution est "maladroite"? Personnellement, je préférerais un constructeur avec des valeurs par défaut à plusieurs constructeurs surchargés dans des situations comme la vôtre (Python ne supporte pas la surcharge de méthodes 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 pourrait être plus simple d'utiliser des fonctions d'usine différentes à 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 ...

17
Ferdinand Beyer

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 se moquent du nombre de trous dans le fromage ou de ce que les internes entrent dans la fabrication du fromage. L'utilisateur de votre code veut juste "gouda" ou "parmesean" non?

Alors pourquoi ne pas faire ceci:

# cheese_user.py
from cheeses import make_gouda, make_parmesean

gouda = make_gouda()
paremesean = make_parmesean()

Et ensuite, vous pouvez utiliser l’une des méthodes ci-dessus pour implémenter 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 à la dactylographie du canard. Vous demandez simplement un objet de gouda et vous ne vous souciez pas vraiment de la classe.

17
Brad C

Il faut absolument préférer les solutions déjà publiées, mais comme personne n’a encore mentionné cette solution, je pense que cela vaut la peine d’être mentionné pour l’exhaustivité.

L'approche @classmethod 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 à l'aide de __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
13
Andrzej Pronobis

La meilleure réponse est celle ci-dessus concernant les arguments par défaut, mais je me suis amusé à l'écrire, et cela convient certainement pour "plusieurs constructeurs". À utiliser à vos risques et périls.

Qu'en est-il de la méthode new .

"Les implémentations typiques créent une nouvelle instance de la classe en appelant la méthode new () de la superclasse à l'aide de super (currentclass, cls) .new (cls [ ...]) avec les arguments appropriés, puis l'instance nouvellement créée selon les besoins avant de la renvoyer. "

Vous pouvez donc faire en sorte que la méthode new modifie la définition de votre classe en attachant la méthode du 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 __== "__main__":
    parm = Cheese(num_holes=5)
9
mluebke

Utilisez plutôt num_holes=None par défaut. Ensuite, vérifiez si num_holes is None et, le cas échéant, de manière aléatoire. 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
Devin Jeanpierre

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

Puisque ma réponse initiale a été critiquée sur la base que mes constructeurs spéciaux n'ont pas appelé le constructeur (unique) par défaut, je publie ici une version modifiée qui respecte les souhaits de tous les constructeurs d'appeler le constructeur un:

class Cheese:
    def __init__(self, *args, _initialiser="_default_init", **kwargs):
        """A multi-initialiser.
        """
        getattr(self, _initialiser)(*args, **kwargs)

    def _default_init(self, ...):
        """A user-friendly smart or general-purpose initialiser.
        """
        ...

    def _init_parmesan(self, ...):
        """A special initialiser for Parmesan cheese.
        """
        ...

    def _init_gauda(self, ...):
        """A special initialiser for Gauda cheese.
        """
        ...

    @classmethod
    def make_parmesan(cls, *args, **kwargs):
        return cls(*args, **kwargs, _initialiser="_init_parmesan")

    @classmethod
    def make_gauda(cls, *args, **kwargs):
        return cls(*args, **kwargs, _initialiser="_init_gauda")
0
Alexey
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
Alexey

Voici comment je l'ai résolu pour une classe YearQuarter que je devais créer. J'ai créé un __init__ avec un seul paramètre appelé value. Le code pour le __init__ décide simplement du type de paramètre value et traite les données en conséquence. Si vous voulez plusieurs paramètres d’entrée, il vous suffit de les regrouper dans un seul Tuple et d’essayer de définir value comme 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 à quoi ressemblent le __init__ et le reste de la classe:

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 bien sûr développer le __init__ avec plusieurs messages d'erreur. Je les ai omis pour cet exemple. 

0
Elmex80s

C'est assez propre je suppose et difficile

class A(object):
    def __init__(self,e,f,g):

        self.__dict__.update({k: v for k,v in locals().items() if k!='self'})
    def bc(self):
        print(self.f)


k=A(e=5,f=6,g=12)
k.bc() # >>>6
0
Amin Pial