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

匿名 (未验证) 提交于 2019-12-03 02:05:01

问题:

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

Suppose I have an class called Cheese with the number_of_holes property. How can I have two ways of creating cheese-objects...

  • one that takes a number of holes like this: parmesan = Cheese(num_holes = 15)
  • and one that takes no arguments and just randomizes the number_of_holes property: gouda = Cheese()

I can think of only one way to do this, but that seems kinda clunky:

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

What do you say? Is there another way?

回答1:

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



回答2:

Using num_holes=None as the default is fine if you are going to have just __init__.

If you want multiple, independent "constructors", you can provide these as class methods. These are usually called factory methods. In this case you could have the default for num_holes be 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)) 

Now create object like this:

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


回答3:

All of these answers are excellent if you want to use optional parameters, but another Pythonic possibility is to use a classmethod to generate a factory-style pseudo-constructor:

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


回答4:

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



回答5:

Those are good ideas for your implementation, but if you are presenting a cheese making interface to a user. They don't care how many holes the cheese has or what internals go into making cheese. The user of your code just wants "gouda" or "parmesean" right?

So why not do this:

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

And then you can use any of the methods above to actually implement the functions:

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

This is a good encapsulation technique, and I think it is more Pythonic. To me this way of doing things fits more in line more with duck typing. You are simply asking for a gouda object and you don't really care what class it is.



回答6:

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) 


回答7:

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.



回答8:

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__         obj._value = load_from_somewhere(somename)         return obj 


回答9:

I'd use inheritance. Especially if there are going to be more differences than number of holes. Especially if Gouda will need to have different set of members then 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)  


回答10:

Text from Alex Martelli

(as mentioned by the comment from @ariddell)

OK, I've researched and read the posts (and the FAQ) on how to simulate multiple constructors. Alex Martelli provided the most robust answer by testing the number of *args and executing the appropriate section of code. However in another post he says

This 'overloads' the constructors based on how many arguments are given -- how elegant (and how Pythonic...!) this is, being of course debatable. Overloading on types would be less elegant and less Pythonic, though you could easily extend this idea to do it -- I would discourage it even more strongly.

I think this part of my response is what makes it "robust":-).

In other words, the best answer to this sort of queries is most often: yes, you can do it (and here's how), but there are better approaches (and here they are). I didn't get fully into the "here's how" and "here they are" parts, admittedly.

However, what I need to do is exactly what is being discouraged, that is creating 3 constructors both with 2 arguments where the second argument of each is a different type. The real kicker is that in one of the constructors, I need to check the class of the object to make sure the method is receiving the proper object. I have no problem coding this if this is the way it has to be but if there are more acceptable (and Pythonic) ways to do this, I would appreciate some pointers.

Why do you think you NEED to distinguish your processing based on an argument's type, or class? More likely, what you want to know about an argument (to determine different processing in different cases) is how it BEHAVES -- which you can't do by testing types, or classes; rather, you may use hasattr or try/except to find out.

By focusing on behavior, rather than on type-identity, you make life easier for the client-code programmer: he or she can then make polymorphic use of your components with any instance that implements the needed behavior, which IS all your code needs.

The idea of 'overloading' -- having a callable with a single given name that maps to multiple internal callables depending on various conditions -- is also related to polymorphism; the only good reason to supply a single callable that maps to multiple ones is to let client-code use that single callable polymorphically if need be.

It's generally a good idea to ALSO expose the multiple callables directly -- don't make client-code programmers go through strange contortions to make sure the 'right' overload is invoked in the end; when their need are non-polymorphic, let them explicitly state as much in the simplest possible way. This does not sit in well with 'constructors' -- which is why factory functions tend to be preferable whenever any application need of some richness and complexity is involved (factory callables that are not functions are also OK, but meet a rarer, yet-more-involved need).

In Python (just like in VB, and other languages with the concept of explicitly-named and default-valued arguments) you have another stylistic alternative to 'multiple callables': one callable can be explicitly used for several related purposes by supplying different named-arguments. This can easily be overdone (and VB supplies a LOT of examples of this style being overused!-) but, used with taste and moderation, it can be very helpful too.

Let's try to see one typical example. We want to expose a class Munger, whose instances need to be initialized with 'a lot of data to be munged'; the 'lot of data' could be a file, a string, or an instance of our own class DataBuffer which provides the data-access features Munger instances need -- in fact, when we are given a file or string, we construct a DataBuffer ourselves and hold that anyway.

The 'overload' style might be:

class Munger:     def __init__(self, data):         name = type(data).__name__         if name=='instance':             name = data.__class__.__name__         method = getattr(self, '_init_'+name)         method(data)     def _init_string(self, data):         self.data = DataBuffer(data)     def _init_file(self, data):         self.data = DataBuffer(data)     def _init_DataBuffer(self, data):         self.data = data 

Now, this IS intended as a 'bad example', and maybe I've overdone the badness, but I hope at least it IS clear why doing it this way would be heavily sub-optimal. This does not exploit in any way the polymorphism of DataBuffer's own constructor, AND it seriously inhibits polymorphism capabilities of client-code (except via such tricks as naming a class as, say, 'string'...!).

It's clearly simpler to frame this as 'a Munger needs to be passed a DataBuffer, or anything a DataBuffer may be built from':

class Munger:     def __init__(self, data):         if not isinstance(data, DataBuffer):             data = DataBuffer(data)         self.data = data 

at least, we have some simplicity here. Polymorphism is still not optimal, though; if client-code wants to mimic a data buffer, it needs to inherit from our DataBuffer class, even if it's not using any of its implementation, just to satisfy our isinstance check. At the very least, one would 'split out' from DataBuffer the interface and implementation parts:

class IDataBuffer:     def rewind(self):         raise TypeError, "must override .rewind method"     def nextBytes(self, N):         raise TypeError, "must override .nextBytes method"     def pushBack(self, bytes):         raise TypeError, "must override .pushBack method" 

etc, with class DataBuffer inheriting from this (and providing the needed overrides, of course) and the isinstance check done against IDataBuffer. Not very Pythonic, but workable if there are a LOT of DataBuffer methods we need -- checking for each of them separately may become more trouble than it's worth.

DataBuffer's own 'overloading' ("am I being initialized from a file or from a string?") needs to be handled. Once again, it would be seriously wrong to code:

class DataBuffer(IDataBuffer):     def __init__(self, data):         name = type(data).__name__         if name=='instance':             name = data.__class__.__name__         method = getattr(self, '_init_'+name)         method(data)     def _init_string(self, data):         self.data = data         self.index = 0     def _init_file(self, data):         self.data = data.read()         self.index = 0     # etc etc 

because it horribily inhibits client-code's polymorphism. Here, all we need from a 'file object' is a .read method we can call without arguments to supply our data -- so why not code that directly...:

class DataBuffer(IDataBuffer):     def __init__(self, data):         try: self.data = data.read()         except AttributeError: self.data=data         self.index = 0     # etc etc 

this is MUCH simpler, of course. One may add some tests at initialization to ensure the resulting data are usable for our purposes, but it's generally no big problem if the error (if any) comes at first usage rather than at initialization.

An alternative architecture is also worth considering. DOES client code REALLY NEED the polymorphism implicit in passing a Munger constructor, either a file(-like) object, or a string(-like) one, with very different implied semantics regarding how one gets data from said object? Python libraries give us counterexamples of that -- file-like objects and string-like ones are generally passed through separate methods; there's no real polymorphism opportunity there!

So...:

class DataBuffer(IDataBuffer):     def __init__(self, data):         self.data = data         self.index = 0     # etc etc  class Munger:     def __init__(self, data):         self.data = data     # etc etc  def FileMunger(afile):     return Munger(DataBuffer(afile.read()))  def StringMunger(astring):     return Munger(DataBuffer(astring)) 

There, isn't THIS better? Two non-overloaded factory functions, maximal simplicity in the constructors proper.

Client-code knows what it IS using to construct the Munger and doesn't need the polymorphism -- it will be clearer and more explicit and readable if it calls FileMunger or StringMunger appropriately, and only uses Munger's ctor directly for those cases where it needs to reuse some existing IDataBuffer instance.

If very occasionally a polymorphic use may benefit the client-code author, we can add a further factory function for that purpose only:

def AnyMunger(mystery):     if isinstance(mystery, IDataBuffer):         return Munger(mystery)     else:         try: return FileMunger(mystery)         except AttributeError: return StringMunger(mystery) 

However, one doesn't go around just adding such stuff unless its appropriateness is clearly shown by some specific use-case/scenario -- "you ain't gonna need it" is a GREAT design principle:-) [XP rules...!-)].

Now, this IS of course a toy-level example, but I hope that just because of this it may show up the issues more clearly -- and perhaps convince you to rethink your design in simpler and more usable ways.

Alex



回答11:

This is how I solved it for a YearQuarter class I had to create. I created an __init__ with a single parameter called value. The code for the __init__ just decides what type the value parameter is and process the data accordingly. In case you want multiple input parameters you just pack them into a single tuple and test for value being a tuple.

You use it like this:

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

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

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) 

You can expand the __init__ with multiple error messages of course. I omitted them for this example.



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


标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!