Wat is een schone, Pythonische manier om meerdere constructors in Python te hebben?

Ik kan hier geen definitief antwoord op vinden. Voor zover ik weet, kun je niet meerdere __init__-functies in een Python-klasse hebben. Dus hoe los ik dit probleem op?

Stel dat ik een klasse heb met de naam Cheesemet de eigenschap number_of_holes. Hoe kan ik op twee manieren kaasobjecten maken…

  1. Een die een aantal gaten neemt zoals deze: parmesan = Cheese(num_holes = 15)
  2. En een die geen argumenten aanneemt en gewoon de eigenschap number_of_holesrandomiseert: gouda = Cheese()

Ik kan maar één manier bedenken om dit te doen, maar dit lijkt onhandig:

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

Wat zeg je? Is er een andere manier?


Antwoord 1, autoriteit 100%

Eigenlijk is Noneveel beter voor “magische” waarden:

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

Als je nu de volledige vrijheid wilt om meer parameters toe te voegen:

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

Om het concept van *argsen **kwargsbeter uit te leggen (je kunt deze namen eigenlijk veranderen):

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


Antwoord 2, autoriteit 91%

Het gebruik van num_holes=Noneals standaard is prima als je alleen __init__wilt hebben.

Als u meerdere, onafhankelijke “constructors” wilt, kunt u deze als klassenmethoden opgeven. Dit worden meestal fabrieksmethoden genoemd. In dit geval zou de standaard voor num_holes0kunnen zijn.

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

Maak nu een object als volgt:

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

Antwoord 3, autoriteit 5%

Je zou zeker de voorkeur moeten geven aan de oplossingen die al zijn gepost, maar aangezien nog niemand deze oplossing heeft genoemd, denk ik dat het voor de volledigheid het vermelden waard is.

De @classmethod-benadering kan worden gewijzigd om een ​​alternatieve constructor te verschaffen die niet de standaardconstructeur (__init__) oproept. In plaats daarvan wordt een exemplaar gemaakt met __new__.

Dit kan worden gebruikt als het type initialisatie niet kan worden geselecteerd op basis van het type van het Constructor-argument, en de constructeurs delen geen code.

Voorbeeld:

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__
        super(MyClass, obj).__init__()  # Don't forget to call any polymorphic base class initializers
        obj._value = load_from_somewhere(somename)
        return obj

4, Autoriteit 3%

Al deze antwoorden zijn uitstekend als u optionele parameters wilt gebruiken, maar een andere pythonische mogelijkheid is om een ​​klasmethod te gebruiken om een ​​fabrieks-stijl pseudo-constructor te genereren:

def __init__(self, num_holes):
  # do stuff with the number
@classmethod
def fromRandom(cls):
  return cls( # some-random-number )

5, Autoriteit 2%

Waarom denk je dat je oplossing “Clunky” is? Persoonlijk zou ik de voorkeur geven aan één constructeur met standaardwaarden over meerdere overbelaste constructeurs in situaties zoals de jouwe (Python ondersteunt toch geen methode overbelasting):

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

Voor echt complexe gevallen met veel verschillende constructeurs, kan het schoner zijn om in plaats daarvan verschillende fabrieksfuncties te gebruiken:

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

In uw kaas kan u misschien een Gouda-subklasse van kaas gebruiken …


6, Autoriteit 2%

Dat zijn goede ideeën voor uw implementatie, maar als u een kaasinterface voor een gebruiker presenteert. Het maakt niet uit hoeveel gaten de kaas heeft of welke internals in het maken van kaas gaan. De gebruiker van uw code wil gewoon “Gouda” of “Parmeese”?

Dus waarom niet doen:

# cheese_user.py
from cheeses import make_gouda, make_parmesean
gouda = make_gouda()
paremesean = make_parmesean()

En dan kunt u een van de bovenstaande methoden gebruiken om de functies te implementeren:

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

Dit is een goede inkapselingstechniek, en ik denk dat het meer pythonic is. Voor mij past deze manier om dingen te doen, meer in de rij meer met eendentypen. Je vraagt ​​gewoon om een ​​Gouda-object en je maakt het niet echt om welke klas het is.


7

Gebruik in plaats daarvan num_holes=Noneals standaard. Controleer vervolgens of num_holes is Noneis, en zo ja, randomiseer. Dat is in ieder geval wat ik over het algemeen zie.

radicaal verschillende constructiemethoden kunnen een classmethod rechtvaardigen die een instantie van clsretourneert.


Antwoord 8

Het beste antwoord is het bovenstaande over standaardargumenten, maar ik vond het leuk om dit te schrijven, en het past zeker bij “meerdere constructeurs”. Gebruik op eigen risico.

Hoe zit het met de nieuwemethode .

“Typische implementaties creëren een nieuwe instantie van de klasse door de methode new() van de superklasse aan te roepen met behulp van super(currentclass, cls).new(cls[, .. .]) met de juiste argumenten en wijzig vervolgens de nieuw gemaakte instantie indien nodig voordat deze wordt geretourneerd.”

Je kunt dus de nieuwemethode je klassendefinitie laten wijzigen door de juiste constructormethode toe te voegen.

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)

Antwoord 9

Ik zou erfenis gebruiken. Vooral als er meer verschillen zijn dan het aantal gaten. Vooral als Gouda een andere set leden moet hebben, dan ParmeSean.

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

10

Dit is hoe ik het heb opgelost voor een YearQuarterklasse ik moest maken. Ik heb een __init__gemaakt die zeer tolerant is voor een grote verscheidenheid aan input.

u gebruikt deze als volgt:

>>> from datetime import date
>>> temp1 = YearQuarter(year=2017, month=12)
>>> print temp1
2017-Q4
>>> temp2 = YearQuarter(temp1)
>>> print temp2
2017-Q4
>>> temp3 = YearQuarter((2017, 6))
>>> print temp3
2017-Q2 
>>> temp4 = YearQuarter(date(2017, 1, 18))
>>> print temp4
2017-Q1
>>> temp5 = YearQuarter(year=2017, quarter = 3)
>>> print temp5
2017-Q3

En dit is hoe de __init__en de rest van de klas eruit zien:

import datetime
class YearQuarter:
    def __init__(self, *args, **kwargs):
        if len(args) == 1:
            [x]     = args
            if isinstance(x, datetime.date):
                self._year      = int(x.year)
                self._quarter   = (int(x.month) + 2) / 3
            elif isinstance(x, tuple):
                year, month     = x
                self._year      = int(year)
                month           = int(month)
                if 1 <= month <= 12:
                    self._quarter   = (month + 2) / 3
                else:
                    raise ValueError
            elif isinstance(x, YearQuarter):
                self._year      = x._year
                self._quarter   = x._quarter
        elif len(args) == 2:
            year, month     = args
            self._year      = int(year)
            month           = int(month)
            if 1 <= month <= 12:
                self._quarter   = (month + 2) / 3
            else:
                raise ValueError
        elif kwargs:
            self._year      = int(kwargs["year"])
            if "quarter" in kwargs:
                quarter     = int(kwargs["quarter"])
                if 1 <= quarter <= 4:
                    self._quarter     = quarter
                else:
                    raise ValueError
            elif "month" in kwargs:
                month   = int(kwargs["month"])
                if 1 <= month <= 12:
                    self._quarter     = (month + 2) / 3
                else:
                    raise ValueError
    def __str__(self):
        return '{0}-Q{1}'.format(self._year, self._quarter)

11

sinds mijn eerste antwoord werd bekritiseerd Op basis dat mijn special-purpose constructors de (unieke) standaardconstructor niet noem, post ik hier een gewijzigde versie die de wensen onderscheidt die alle constructeurs de standaard één noemen:

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_gouda(self, ...):
        """A special initialiser for Gouda cheese.
        """
        ...
    @classmethod
    def make_parmesan(cls, *args, **kwargs):
        return cls(*args, **kwargs, _initialiser="_init_parmesan")
    @classmethod
    def make_gouda(cls, *args, **kwargs):
        return cls(*args, **kwargs, _initialiser="_init_gouda")

12

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

Other episodes