我找不到一个明确的答案。据我所知,在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

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


当前回答

如果你只需要__init__,使用num_holes=None作为默认值是可以的。

如果你想要多个独立的“构造函数”,你可以把它们作为类方法提供。这些方法通常称为工厂方法。在本例中,num_holes的默认值为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))

现在创建一个这样的对象:

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

其他回答

这些对于你的实现来说都是很好的想法,但是如果你要向用户展示一个奶酪制作界面。他们不关心奶酪有多少洞,也不关心奶酪的内部成分。你代码的用户只想要“豪达干酪”或“帕尔马干酪”,对吧?

所以为什么不这样做呢:

# cheese_user.py
from cheeses import make_gouda, make_parmesean

gouda = make_gouda()
paremesean = make_parmesean()

然后你可以使用上面的任何方法来实际实现这些函数:

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

这是一种很好的封装技术,而且我认为它更具有python性。对我来说,这种做事的方式更符合鸭子的打字方式。你只是在请求一个gouda对象,而不关心它是什么类。

最好的答案是上面关于默认参数的那个,但我很高兴写这个,而且它确实符合“多个构造函数”的要求。使用风险自负。

新方法怎么样?

典型的实现通过使用super(currentclass, cls)调用超类的new()方法来创建类的新实例。使用适当的参数New (cls[,…]),然后在返回它之前根据需要修改新创建的实例。”

因此,您可以让新方法通过附加适当的构造函数方法来修改类定义。

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)

这里(引用前面的答案,文档中classmethod的纯Python版本,正如这条评论所建议的那样)是一个可以用来创建多个构造函数的装饰器。

from types import MethodType
from functools import wraps

class constructor:
    def __init__(self, func):

        @wraps(func)                      
        def wrapped(cls, *args, **kwargs):
            obj = cls.__new__(cls)        # Create new instance but don't init
            super(cls, obj).__init__()    # Init any classes it inherits from
            func(obj, *args, **kwargs)    # Run the constructor with obj as self
            return obj                
        
        self.wrapped = wrapped

    def __get__(self, _, cls):
        return MethodType(self.wrapped, cls)   # Bind this constructor to the class 
        
    
class Test:
    def __init__(self, data_sequence):
        """ Default constructor, initiates with data sequence """
        self.data = [item ** 2 for item in data_sequence]
        
    @constructor
    def zeros(self, size):
        """ Initiates with zeros """
        self.data = [0 for _ in range(size)]
           
a = Test([1,2,3])
b = Test.zeros(100)

在某些情况下,这似乎是最干净的方法(例如,Pandas中的多个dataframe构造函数),在这些情况下,为单个构造函数提供多个可选参数将是不方便的:例如,它将需要太多参数,不可读,速度较慢或使用更多的内存。然而,正如前面的评论所指出的,在大多数情况下,通过一个带有可选参数的构造函数路由,在需要的地方添加类方法可能更符合python的规则。

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

概述

对于特定的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.