ChatGPT解决这个技术问题 Extra ChatGPT

How to overload __init__ method based on argument type?

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.

@And or vice versa? (I mean this is the older question)
@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.

T
Thomas Wouters

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.


Cool! Where can I read about what exactly @classmethod does under the hood?
where have you defined the behavior of cls()?
@Ajay See this question for clarification
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?
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!
F
FMc

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


how is obj.read(str="\x34\x55") is handled; you do not have code that will handle when str is not None
@brainstorm I think the code that handles non-None string lies in the "# rest of code". :-)
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.
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.
I personally prefer more explicit solution, where you have one constructor per type. This makes your code easier to read, maintain and change.
c
carton.swing

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

This idea of using metaclass to construct multiple __init__ function is interesting, could you please explain the principles behind this?
@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 for more details.
@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.
@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, thanks for replying. No, I didn't use MultipleMeta.
B
Ben

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

The second might be better written as MyData(list = alist).
This is the best solution I believe. I've expanded on it with some more detail if you care to take a look: stackoverflow.com/a/26018762/385025
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.
This is more like a work around, doesn't properly answer the question
J
Justin

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)

M
Moe

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

E
Eli Courtwright

You probably want the isinstance builtin function:

self.data = data if isinstance(data, list) else self.parse(data)

the if else expression will only work in python 2.5 (and later)
B
Baltimark

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]


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

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?

Never control flow of execution by forcing errors with try catch. This is a pretty standard rule for all programming languages.
No, in Python frequently (but not always) it's the other way around: stackoverflow.com/questions/12265451/… And in this case it's is really much more cheap to do it like that.
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.
It's not only CPU time to consider (which i pretty well understand stackoverflow.com/questions/2522005/…); 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: docs.python.org/3.6/glossary.html#term-eafp
F
Fydo

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!


I'm guessing its because having both _data and self._data us unclear.
The _data class variable does not make sense in this example.Maybe you have some misconception in regards of the meaning of "_data".
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.