我找不到一个明确的答案。据我所知,在Python类中不能有多个__init__函数。那么如何解决这个问题呢?

假设我有一个名为Cheese的类,它具有number_of_holes属性。我怎么能有两种方法来创建奶酪对象…

其中一个需要像这样的洞的数量:帕玛森=奶酪(num_holes = 15)。 还有一个不带参数,只是随机number_of_holes属性:gouda = Cheese()。

我只能想到一种方法来做到这一点,但这似乎很笨拙:

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

你说呢?还有别的办法吗?


当前回答

概述

对于特定的cheese示例,我同意使用默认值来表示随机初始化或使用静态工厂方法的许多其他答案。但是,在您想到的相关场景中,使用其他简洁的方法调用构造函数而不影响形参名称或类型信息的质量是有价值的。

自Python 3.8和functools开始。在许多情况下,Singledispatchmethod可以帮助实现这一点(更灵活的multimethod可以应用于更多的场景)。(这篇相关文章描述了如何在没有库的情况下在Python 3.4中实现同样的功能。)我还没有在文档中看到这两种方法的例子,具体显示重载__init__,因为你问,但似乎重载任何成员方法的相同原则适用(如下所示)。

"Single dispatch" (available in the standard library) requires that there be at least one positional parameter and that the type of the first argument be sufficient to distinguish among the possible overloaded options. For the specific Cheese example, this doesn't hold since you wanted random holes when no parameters were given, but multidispatch does support the very same syntax and can be used as long as each method version can be distinguish based on the number and type of all arguments together.

例子

下面是一个如何使用这两种方法的例子(一些细节是为了取悦我的ypy,这是我第一次把这些放在一起的目标):

from functools import singledispatchmethod as overload
# or the following more flexible method after `pip install multimethod`
# from multimethod import multidispatch as overload


class MyClass:

    @overload  # type: ignore[misc]
    def __init__(self, a: int = 0, b: str = 'default'):
        self.a = a
        self.b = b

    @__init__.register
    def _from_str(self, b: str, a: int = 0):
        self.__init__(a, b)  # type: ignore[misc]

    def __repr__(self) -> str:
        return f"({self.a}, {self.b})"


print([
    MyClass(1, "test"),
    MyClass("test", 1),
    MyClass("test"),
    MyClass(1, b="test"),
    MyClass("test", a=1),
    MyClass("test"),
    MyClass(1),
    # MyClass(),  # `multidispatch` version handles these 3, too.
    # MyClass(a=1, b="test"),
    # MyClass(b="test", a=1),
])

输出:

[(1, test), (1, test), (0, test), (1, test), (1, test), (0, test), (1, default)]

注:

I wouldn't usually make the alias called overload, but it helped make the diff between using the two methods just a matter of which import you use. The # type: ignore[misc] comments are not necessary to run, but I put them in there to please mypy which doesn't like decorating __init__ nor calling __init__ directly. If you are new to the decorator syntax, realize that putting @overload before the definition of __init__ is just sugar for __init__ = overload(the original definition of __init__). In this case, overload is a class so the resulting __init__ is an object that has a __call__ method so that it looks like a function but that also has a .register method which is being called later to add another overloaded version of __init__. This is a bit messy, but it please mypy becuase there are no method names being defined twice. If you don't care about mypy and are planning to use the external library anyway, multimethod also has simpler alternative ways of specifying overloaded versions. Defining __repr__ is simply there to make the printed output meaningful (you don't need it in general). Notice that multidispatch is able to handle three additional input combinations that don't have any positional parameters.

其他回答

我还没有看到一个直截了当的答案。想法很简单:

使用__init__作为“基本”构造函数,因为python只允许一个__init__方法 使用@classmethod创建任何其他构造函数并调用基本构造函数

这是一个新的尝试。

 class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    @classmethod
    def fromBirthYear(cls, name, birthYear):
        return cls(name, date.today().year - birthYear)

用法:

p = Person('tim', age=18)
p = Person.fromBirthYear('tim', birthYear=2004)

人们肯定更喜欢已经发布的解决方案,但由于还没有人提到这个解决方案,我认为为了完整性,值得一提。

@classmethod方法可以修改为提供一个不调用默认构造函数(__init__)的替代构造函数。相反,使用__new__创建实例。

如果不能根据构造函数参数的类型选择初始化类型,并且构造函数不共享代码,则可以使用此方法。

例子:

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

实际上,对于“魔幻”价值观来说,None要好得多:

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

现在,如果你想要完全自由地添加更多参数:

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

为了更好地解释*args和**kwargs的概念(实际上您可以更改这些名称):

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

为什么你认为你的解决方案“笨拙”?就我个人而言,在你这样的情况下,我更喜欢一个具有默认值的构造函数,而不是多个重载构造函数(Python不支持方法重载):

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

对于有很多不同构造函数的非常复杂的情况,使用不同的工厂函数可能会更干净:

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

@classmethod
def create_cheddar(cls):
    # ...

在你的奶酪的例子中,你可能想要使用奶酪的一个Gouda子类…

使用num_holes=None作为默认值。然后检查num_holes是否为None,如果是,则随机化。这就是我通常看到的情况。

更完全不同的构造方法可能需要一个返回cls实例的类方法。