425

Let's say I have a class that has a member called data which is a list.

I want to be able to initialize the class with, for example, a filename (which contains data to initialize the list) or with an actual list.

What's your technique for doing this?

Do you just check the type by looking at __class__?

Is there some trick I might be missing?

I'm used to C++ where overloading by argument type is easy.

martineau
  • 119,623
  • 25
  • 170
  • 301
Baltimark
  • 9,052
  • 12
  • 37
  • 35
  • 2
    Possible duplicate of [What is a clean, pythonic way to have multiple constructors in Python?](http://stackoverflow.com/questions/682504/what-is-a-clean-pythonic-way-to-have-multiple-constructors-in-python) – Andrzej Pronobis Aug 11 '16 at 00:05
  • 4
    @And or vice versa? (I mean *this* is the older question) – Wolf Sep 21 '16 at 14:11
  • @Wolf I won't say which is the better topic between the two, but older questions often get closed as dupes of newer ones when the newer one is better quality/has better answers/covers the topic in a more broadly applicable way. – TylerH Mar 14 '18 at 14:38

10 Answers10

553

A much neater way to get 'alternate constructors' is to use classmethods. For instance:

>>> class MyData:
...     def __init__(self, data):
...         "Initialize MyData from a sequence"
...         self.data = data
...     
...     @classmethod
...     def fromfilename(cls, filename):
...         "Initialize MyData from a file"
...         data = open(filename).readlines()
...         return cls(data)
...     
...     @classmethod
...     def fromdict(cls, datadict):
...         "Initialize MyData from a dict's items"
...         return cls(datadict.items())
... 
>>> MyData([1, 2, 3]).data
[1, 2, 3]
>>> MyData.fromfilename("/tmp/foobar").data
['foo\n', 'bar\n', 'baz\n']
>>> MyData.fromdict({"spam": "ham"}).data
[('spam', 'ham')]

The reason it's neater is that there is no doubt about what type is expected, and you aren't forced to guess at what the caller intended for you to do with the datatype it gave you. The problem with isinstance(x, basestring) is that there is no way for the caller to tell you, for instance, that even though the type is not a basestring, you should treat it as a string (and not another sequence.) And perhaps the caller would like to use the same type for different purposes, sometimes as a single item, and sometimes as a sequence of items. Being explicit takes all doubt away and leads to more robust and clearer code.

Thomas Wouters
  • 130,178
  • 23
  • 148
  • 122
  • Cool! Where can I read about what exactly @classmethod does under the hood? – Hamish Grubijan Jun 27 '10 at 16:55
  • 4
    where have you defined the behavior of cls()? – Ajay Feb 24 '15 at 17:22
  • 1
    @Ajay See [this](http://stackoverflow.com/questions/4613000/what-is-the-cls-variable-used-in-python-classes) question for clarification – Kim Ahlstrøm Meyn Mathiassen Apr 15 '15 at 06:39
  • 7
    why not use `@staticmethod` since the `__init__` in this example is doing prettymuch nothing useful, when the goal is to use e.g. `fromfilename` in first place? – omni Sep 10 '18 at 12:55
  • I fought with this for a while, and in the end I ended up creating a base class, and two child classes, each of which had the different init argument list. This to me was more readable. Thanks for the inspiration! – veggiebenz Apr 11 '19 at 17:28
  • Are you purposefully calling them `fromfilename` and not `from_filename`? – theonlygusti Oct 18 '20 at 08:46
  • unfortunately classmethods are not obvious for user. I prefer def `MyData_from_filename()` in the same namespace as `MyData`. In this case user will see such definitions near `MyData` in intellisense hint when accesses `somelib.` – iperov Dec 28 '22 at 17:17
46

with python3, you can use Implementing Multiple Dispatch with Function Annotations as Python Cookbook wrote:

import time


class Date(metaclass=MultipleMeta):
    def __init__(self, year:int, month:int, day:int):
        self.year = year
        self.month = month
        self.day = day

    def __init__(self):
        t = time.localtime()
        self.__init__(t.tm_year, t.tm_mon, t.tm_mday)

and it works like:

>>> d = Date(2012, 12, 21)
>>> d.year
2012
>>> e = Date()
>>> e.year
2018
carton.swing
  • 1,517
  • 17
  • 12
  • 1
    This idea of using metaclass to construct multiple `__init__` function is interesting, could you please explain the principles behind this? – GoingMyWay May 15 '18 at 07:42
  • @GoingMyWay `__prepare__` method in `MultipleMeta` returns a `MultiDict` instance to replace `Date` class default`__dict__` attribute passed by `clsdict` in `__new__` method. therefore, it can hold multi function with the same name '`__init__`', and the value of that is a `MultiMethod` instance, which stores with different function annotation in it’s _method attribute. You should check [*Python Cookbook*](https://books.google.com/books?id=f7wGeA71_eUC&lpg=PA376&dq=Implementing%20Multiple%20Dispatch%20with%20Function%20Annotations&pg=PA376#v=onepage&q&f=true) for more details. – carton.swing May 16 '18 at 01:31
  • 1
    @carton.swing , which version of python supports 'delegating' __init__ ? I tried it with `3.6.8` and it complained `TypeError: __init__() takes 2 positional arguments but 3 were given`. In my code it was `init(self, x)` and `init(self, a,b)` and the latter would be called from the former. – Yuri Pozniak Nov 04 '19 at 23:28
  • @YuriyPozniak Python does not suports 'delegating', it only supports function annotation yet, and you can implement the metaclass by function annotation. Do you use the above metaclass 'MultipleMeta' as the Python Cookbook wrote? – carton.swing Nov 06 '19 at 03:08
  • @carton.swing, thanks for replying. No, I didn't use MultipleMeta. – Yuri Pozniak Nov 06 '19 at 18:46
41

Excellent question. I've tackled this problem as well, and while I agree that "factories" (class-method constructors) are a good method, I would like to suggest another, which I've also found very useful:

Here's a sample (this is a read method and not a constructor, but the idea is the same):

def read(self, str=None, filename=None, addr=0):
    """ Read binary data and return a store object. The data
        store is also saved in the interal 'data' attribute.

        The data can either be taken from a string (str 
        argument) or a file (provide a filename, which will 
        be read in binary mode). If both are provided, the str 
        will be used. If neither is provided, an ArgumentError 
        is raised.
    """
    if str is None:
        if filename is None:
            raise ArgumentError('Please supply a string or a filename')

        file = open(filename, 'rb')
        str = file.read()
        file.close()
    ...
    ... # rest of code

The key idea is here is using Python's excellent support for named arguments to implement this. Now, if I want to read the data from a file, I say:

obj.read(filename="blob.txt")

And to read it from a string, I say:

obj.read(str="\x34\x55")

This way the user has just a single method to call. Handling it inside, as you saw, is not overly complex

FMc
  • 41,963
  • 13
  • 79
  • 132
Eli Bendersky
  • 263,248
  • 89
  • 350
  • 412
  • how is `obj.read(str="\x34\x55")` is handled; you do not have code that will handle when str is not `None` – brain storm Feb 05 '14 at 01:39
  • 3
    @brainstorm I think the code that handles non-None string lies in the "# rest of code". :-) – yaobin Jan 30 '15 at 15:16
  • 7
    One thing that may make this solution not that graceful happens when you want to overload many versions of constructors, such as that you want to construct an object from an integer, OR a file, OR a string, OR... OR... OR... OR... OR... Then you will end up with a very long list of __init__ parameters. – yaobin Jan 30 '15 at 15:22
  • 12
    Another problem is that being the caller, I have no idea that which parameters I should use to construct an object unless I read the documents. In the example above, the caller may provide both str and filename, but only str is considered because it is higher in the if-statement hierarchy. Document can help, but it's better that we can design the interface without ambiguity. – yaobin Jan 30 '15 at 15:24
  • I personally prefer more explicit solution, where you have one constructor per type. This makes your code easier to read, maintain and change. – giorgio Dec 31 '21 at 11:49
15

Quick and dirty fix

class MyData:
    def __init__(string=None,list=None):
        if string is not None:
            #do stuff
        elif list is not None:
            #do other stuff
        else:
            #make data empty

Then you can call it with

MyData(astring)
MyData(None, alist)
MyData()
Ben
  • 669
  • 8
  • 14
  • 11
    The second might be better written as `MyData(list = alist)`. – Vivek Ghaisas Jun 19 '14 at 08:56
  • This is the best solution I believe. I've expanded on it with some more detail if you care to take a look: http://stackoverflow.com/a/26018762/385025 – Fydo Sep 24 '14 at 14:00
  • 7
    Don't you miss `self` in `__init__`? And you might not want to use `list` as input name as it shadows the built-in type `list`. – Cleb May 21 '19 at 20:20
  • This is more like a work around, doesn't properly answer the question – Joabe Lucena Oct 08 '20 at 20:18
10

A better way would be to use isinstance and type conversion. If I'm understanding you right, you want this:

def __init__ (self, filename):
    if isinstance (filename, basestring):
        # filename is a string
    else:
        # try to convert to a list
        self.path = list (filename)
Justin
  • 24,288
  • 12
  • 92
  • 142
John Millikin
  • 197,344
  • 39
  • 212
  • 226
4

You should use isinstance

isinstance(...)
    isinstance(object, class-or-type-or-tuple) -> bool

    Return whether an object is an instance of a class or of a subclass thereof.
    With a type as second argument, return whether that is the object's type.
    The form using a tuple, isinstance(x, (A, B, ...)), is a shortcut for
    isinstance(x, A) or isinstance(x, B) or ... (etc.).
Moe
  • 28,607
  • 10
  • 51
  • 67
2

You probably want the isinstance builtin function:

self.data = data if isinstance(data, list) else self.parse(data)
Eli Courtwright
  • 186,300
  • 67
  • 213
  • 256
0

OK, great. I just tossed together this example with a tuple, not a filename, but that's easy. Thanks all.

class MyData:
    def __init__(self, data):
        self.myList = []
        if isinstance(data, tuple):
            for i in data:
                self.myList.append(i)
        else:
            self.myList = data

    def GetData(self):
        print self.myList

a = [1,2]

b = (2,3)

c = MyData(a)

d = MyData(b)

c.GetData()

d.GetData()

[1, 2]

[2, 3]

Baltimark
  • 9,052
  • 12
  • 37
  • 35
  • There's no need for all that code in __init__ -- I shortened it down to just a type conversion, which does the same thing and is more flexible. – John Millikin Sep 26 '08 at 20:22
  • In Python, the getter is also mostly unnecessary. Just use direct attribute access. If you ever need to do more, you can use property() to hide the getter/setter behind normal attribute access. – Thomas Wouters Sep 26 '08 at 20:30
  • I know that, but that defeats the purpose of the example; I was just trying to show how to use two different input types. It might not be necessary with tuple/list, but it would be if that was a filename. I guess that just echoes what others said, though. My example would have been instructive to me – Baltimark Sep 26 '08 at 20:31
0

My preferred solution is:

class MyClass:
    _data = []
    __init__(self,data=None):
        # do init stuff
        if not data: return
        self._data = list(data) # list() copies the list, instead of pointing to it.

Then invoke it with either MyClass() or MyClass([1,2,3]).

Hope that helps. Happy Coding!

Fydo
  • 1,396
  • 16
  • 29
  • 3
    I'm guessing its because having both `_data` and `self._data` us unclear. – shuttle87 Aug 03 '15 at 23:56
  • 1
    The _data class variable does not make sense in this example.Maybe you have some misconception in regards of the meaning of "_data". – miracle173 May 31 '20 at 00:15
  • The constructor in this example returns instances that either have their own _data list, or refer to the common list in the class variable _data. Once constructed there is no simple way for code to be aware of which behavior a particular instance has. Since the two behaviors are quite different, this seems like a poor idea. – gwideman Jun 10 '20 at 05:14
-1

Why don't you go even more pythonic?

class AutoList:
def __init__(self, inp):
    try:                        ## Assume an opened-file...
        self.data = inp.read()
    except AttributeError:
        try:                    ## Assume an existent filename...
            with open(inp, 'r') as fd:
                self.data = fd.read()
        except:
            self.data = inp     ## Who cares what that might be?
ankostis
  • 8,579
  • 3
  • 47
  • 61
  • 8
    Never control flow of execution by forcing errors with try catch. This is a pretty standard rule for all programming languages. – Jamie Marshall Jun 26 '18 at 00:59
  • 3
    No, in Python frequently (but not always) it's the other way around: https://stackoverflow.com/questions/12265451/ask-forgiveness-not-permission-explain And in this case it's is really much more cheap to do it like that. – ankostis Jun 27 '18 at 15:26
  • 1
    I think you're misunderstanding the basis of try/except. The fundamental way it works is very different from if statements, and every error that gets handled has a very high cpu expense as opposed to other methods of flow control. The link you provided suggests that one should use try/except in places where a variety of errors might occur - I agree. That scenario is totally different however from using try/except to change the flow of a program based on an exception your sure will happen often or intentionally. – Jamie Marshall Jun 27 '18 at 17:04
  • 1
    It's not only CPU time to consider (which i pretty well understand https://stackoverflow.com/questions/2522005/cost-of-exception-handlers-in-python); it's also developer's time, tersity of the code for reviewer to quickly understand it, along with other important coding-style issues. In this 1st case above, the alternative would be: `if inp.hasattr('read') and callable(inp.read): self.data = inp.read()`. The 2nd case would be even more convoluted. In the end, all these might cost more CPU. No surprisingly, python-manual endorses EAFP: https://docs.python.org/3.6/glossary.html#term-eafp – ankostis Jun 28 '18 at 18:06