如何根据参数类型重载 __init__ 方法?

436 投票
10 回答
263194 浏览
提问于 2025-04-11 09:25

假设我有一个类,这个类里面有一个叫做data的成员,它是一个列表。

我想要能够用一个文件名来初始化这个类(这个文件里包含了用来初始化列表的数据),或者直接用一个实际的列表来初始化。

你们通常是怎么做到这一点的呢?

你们是通过查看 __class__ 来检查类型吗?

有没有什么技巧我可能还没想到的?

我习惯于C++,在那里面根据参数类型重载函数是很简单的。

10 个回答

41

这是个很好的问题。我也遇到过这个问题,虽然我同意“工厂”(类方法构造函数)是一种不错的方法,但我想推荐另一种我觉得也很有用的方法:

这里有个示例(这是一个 read 方法,不是构造函数,但思路是一样的):

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

这里的关键点是利用Python对命名参数的优秀支持来实现这个功能。现在,如果我想从文件中读取数据,我可以这样说:

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

如果我想从字符串中读取数据,我可以这样说:

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

这样用户只需要调用一个方法。正如你看到的,内部处理并不复杂。

49

在Python 3中,你可以使用通过函数注解实现多重分发,就像Python Cookbook里写的那样:

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)

它的工作方式是:

>>> d = Date(2012, 12, 21)
>>> d.year
2012
>>> e = Date()
>>> e.year
2018
565

有一种更简洁的方法来获取“备用构造函数”,那就是使用类方法。例如:

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

这样做的好处是,你可以明确知道期望的类型是什么,而不需要猜测调用者希望你如何处理他们给你的数据类型。比如,使用 isinstance(x, basestring) 的问题在于,调用者无法告诉你,尽管类型不是 basestring,但你应该把它当作字符串来处理(而不是其他序列)。而且,调用者可能希望在不同的情况下使用相同的类型,有时作为单个项目,有时作为多个项目的序列。明确地说明这一点可以消除所有疑虑,使代码更加健壮和清晰。

撰写回答