什么是在Python中拥有多个构造函数的干净,pythonic方式?

时间:2009-03-25 17:00:22

标签: python constructor

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

假设我有一个名为Cheese的类,其number_of_holes属性。我怎样才能有两种创建奶酪对象的方法......

  1. 带有这样一些漏洞的人:parmesan = Cheese(num_holes = 15)
  2. 和一个不带参数且只是随机化number_of_holes属性:gouda = Cheese()
  3. 我只想到一种方法,但这似乎有点笨拙:

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

    你怎么说?还有另一种方式吗?

13 个答案:

答案 0 :(得分:740)

实际上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

答案 1 :(得分:581)

如果您只有num_holes=None,则使用__init__作为默认设置即可。

如果你想要多个独立的“构造函数”,你可以将它们作为类方法提供。这些通常称为工厂方法。在这种情况下,您可以将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()

答案 2 :(得分:21)

如果您想使用可选参数,所有这些答案都非常好,但另一种Pythonic可能是使用classmethod生成工厂式伪构造函数:

def __init__(self, num_holes):

  # do stuff with the number

@classmethod
def fromRandom(cls):

  return cls( # some-random-number )

答案 3 :(得分:17)

为什么你认为你的解决方案“笨拙”?就个人而言,我个人更喜欢一个默认值超过多个重载构造函数的构造函数(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子类...

答案 4 :(得分:17)

这些是您实施的好主意,但如果您要向用户展示奶酪制作界面。他们不关心奶酪有多少孔或者是什么内部制作奶酪。您的代码的用户只想要“gouda”或“parmesean”吗?

那么为什么不这样做:

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

这是一种很好的封装技术,我认为它更像是Pythonic。对我来说,这种做事方式更适合鸭子打字。你只是要求一个gouda对象而你并不关心它是什么类。

答案 5 :(得分:14)

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

可以修改@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__
        obj._value = load_from_somewhere(somename)
        return obj

答案 6 :(得分:10)

最好的答案是关于默认参数的上面的答案,但我很乐意写这个,它确实符合“多个构造函数”的法案。使用风险自负。

new方法呢。

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

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

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)

答案 7 :(得分:8)

请改为使用num_holes=None作为默认设置。然后检查是否num_holes is None,如果是,则随机化。无论如何,这就是我通常看到的。

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

答案 8 :(得分:2)

我使用继承。特别是如果存在比孔数更多的差异。特别是如果Gouda需要有不同的成员,那么Parmesan。

class Gouda(Cheese):
    def __init__(self):
        super(Gouda).__init__(num_holes=10)


class Parmesan(Cheese):
    def __init__(self):
        super(Parmesan).__init__(num_holes=15) 

答案 9 :(得分:1)

概述

对于特定的奶酪示例,我同意许多其他关于使用默认值来发出随机初始化信号或使用静态工厂方法的答案。但是,您可能还想到了一些相关的场景, 在不损害参数名称或类型信息的质量的情况下,以替代、简洁的方式调用构造函数是有价值的。

由于 Python 3.8 和 functools.singledispatchmethod 可以在许多情况下帮助实现这一点(更灵活的 multimethod 可以应用于更多场景)。 (This related post 描述了如何在没有库的情况下在 Python 3.4 中完成相同的工作。)我没有在文档中看到任何一个示例,这些示例专门显示了您询问的重载 __init__,但它似乎适用于重载任何成员方法的相同原则(如下所示)。

“单一调度”(在标准库中可用)要求至少有一个位置参数并且第一个参数的类型足以区分可能的重载选项。对于特定的 Cheese 示例,这不成立,因为在没有给定参数时您想要随机孔,但是 multidispatch 确实支持非常相同的语法并且可以使用只要每个方法版本可以根据所有参数的数量和类型。

示例

以下是如何使用任一方法的示例(一些细节是为了取悦mypy,这是我第一次将其放在一起时的目标):

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

注意事项:

  • 我通常不会将别名命名为 overload,但它有助于区分使用两种方法之间的差异,这只是您使用哪个导入的问题。
  • # type: ignore[misc] 注释不需要运行,但我把它们放在那里是为了取悦 mypy,它不喜欢装饰 __init__ 也不喜欢直接调用 __init__。< /li>
  • 如果您不熟悉装饰器语法,请意识到将 @overload 放在 __init__ 的定义之前只是 __init__ = overload(the original definition of __init__) 的糖。在这种情况下,overload 是一个类,因此结果 __init__ 是一个具有 __call__ 方法的对象,因此它看起来像一个函数,但也有一个 .register 方法稍后将调用它以添加另一个重载版本的 __init__。这有点混乱,但它请我的py 因为没有被定义两次的方法名称。如果您不关心 mypy 并且无论如何都打算使用外部库,multimethod 也有更简单的替代方法来指定重载版本。
  • 定义 __repr__ 只是为了使打印输出有意义(您通常不需要它)。
  • 请注意,multidispatch 能够处理三个没有任何位置参数的额外输入组合。

答案 10 :(得分:0)

这就是我为必须创建的YearQuarter类解决它的方法。我使用名为__init__的单个参数创建了value__init__的代码只决定value参数的类型并相应地处理数据。如果你想要多个输入参数,你只需将它们打包成一个元组并测试value是否为元组。

你这样使用它:

>>> temp = YearQuarter(datetime.date(2017, 1, 18))
>>> print temp
2017-Q1
>>> temp = YearQuarter((2017, 1))
>>> print temp
2017-Q1

这就是__init__和班级其他人的样子:

import datetime


class YearQuarter:

    def __init__(self, value):
        if type(value) is datetime.date:
            self._year = value.year
            self._quarter = (value.month + 2) / 3
        elif type(value) is tuple:               
            self._year = int(value[0])
            self._quarter = int(value[1])           

    def __str__(self):
        return '{0}-Q{1}'.format(self._year, self._quarter)

您当然可以使用多条错误消息展开__init__。我为这个例子省略了它们。

答案 11 :(得分:0)

我猜这很干净,很棘手

class A(object):
    def __init__(self,e,f,g):

        self.__dict__.update({k: v for k,v in locals().items() if k!='self'})
    def bc(self):
        print(self.f)


k=A(e=5,f=6,g=12)
k.bc() # >>>6

答案 12 :(得分:0)

由于my initial answer被批评on the basis,因为我的专用构造函数没有调用(唯一的)默认构造函数,因此我在此处发布了一个修改版本,以兑现所有构造函数都应调用默认构造函数的愿望。 :

class Cheese:
    def __init__(self, *args, _initialiser="_default_init", **kwargs):
        """A multi-initialiser.
        """
        getattr(self, _initialiser)(*args, **kwargs)

    def _default_init(self, ...):
        """A user-friendly smart or general-purpose initialiser.
        """
        ...

    def _init_parmesan(self, ...):
        """A special initialiser for Parmesan cheese.
        """
        ...

    def _init_gouda(self, ...):
        """A special initialiser for Gouda cheese.
        """
        ...

    @classmethod
    def make_parmesan(cls, *args, **kwargs):
        return cls(*args, **kwargs, _initialiser="_init_parmesan")

    @classmethod
    def make_gouda(cls, *args, **kwargs):
        return cls(*args, **kwargs, _initialiser="_init_gouda")