What is a clean, pythonic way to have multiple constructors in Python?

后端 未结 13 2095
醉梦人生
醉梦人生 2020-11-22 07:12

I can\'t find a definitive answer for this. As far as I know, you can\'t have multiple __init__ functions in a Python class. So how do I solve this problem?

相关标签:
13条回答
  • 2020-11-22 07:27

    This is how I solved it for a YearQuarter class I had to create. I created an __init__ which is very tolerant to a wide variety of input.

    You use it like this:

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

    And this is how the __init__ and the rest of the class looks like:

    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)
    
    0 讨论(0)
  • 2020-11-22 07:28

    Why do you think your solution is "clunky"? Personally I would prefer one constructor with default values over multiple overloaded constructors in situations like yours (Python does not support method overloading anyway):

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

    For really complex cases with lots of different constructors, it might be cleaner to use different factory functions instead:

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

    In your cheese example you might want to use a Gouda subclass of Cheese though...

    0 讨论(0)
  • 2020-11-22 07:30

    The best answer is the one above about default arguments, but I had fun writing this, and it certainly does fit the bill for "multiple constructors". Use at your own risk.

    What about the new method.

    "Typical implementations create a new instance of the class by invoking the superclass’s new() method using super(currentclass, cls).new(cls[, ...]) with appropriate arguments and then modifying the newly-created instance as necessary before returning it."

    So you can have the new method modify your class definition by attaching the appropriate constructor method.

    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)
    
    0 讨论(0)
  • 2020-11-22 07:35

    One should definitely prefer the solutions already posted, but since no one mentioned this solution yet, I think it is worth mentioning for completeness.

    The @classmethod approach can be modified to provide an alternative constructor which does not invoke the default constructor (__init__). Instead, an instance is created using __new__.

    This could be used if the type of initialization cannot be selected based on the type of the constructor argument, and the constructors do not share code.

    Example:

    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
    
    0 讨论(0)
  • 2020-11-22 07:37

    Actually None is much better for "magic" values:

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

    Now if you want complete freedom of adding more parameters:

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

    To better explain the concept of *args and **kwargs (you can actually change these names):

    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

    0 讨论(0)
  • 2020-11-22 07:40

    Use num_holes=None as a default, instead. Then check for whether num_holes is None, and if so, randomize. That's what I generally see, anyway.

    More radically different construction methods may warrant a classmethod that returns an instance of cls.

    0 讨论(0)
提交回复
热议问题