什么是实现多个构造函数的干净的“pythonic”方式?

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

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

  1. 一个需要像这样的多个孔:parmesan = Cheese(num_holes = 15)
  2. 一个不需要参数,只是随机化number_of_holes属性:gouda = Cheese()

我能想到的只有一种方法,但这似乎很笨拙:

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

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

431056 次浏览

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

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

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

如果你只需要__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
@classmethoddef random(cls):return cls(randint(0, 100))
@classmethoddef slightly_holey(cls):return cls(randint(0, 33))
@classmethoddef very_holey(cls):return cls(randint(66, 100))

现在像这样创建对象:

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

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

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

对于具有许多不同构造函数的非常复杂的情况,使用不同的工厂函数可能会更清晰:

@classmethoddef create_gouda(cls):c = Cheese()# ...return c
@classmethoddef create_cheddar(cls):# ...

在您的奶酪示例中,您可能希望使用Cheese的Gouda子类…

如果你想使用可选参数,所有这些答案都很好,但Pythonic的另一种可能性是使用类方法生成工厂式伪构造函数:

def __init__(self, num_holes):
# do stuff with the number
@classmethoddef fromRandom(cls):
return cls( # some-random-number )

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

什么是<强>新方法?

典型的实现通过调用超类的新的()方法来创建类的新实例,方法是:使用带有适当参数的超(当前类,cls).新的(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.foomethodelse: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)

这些对你的实现来说都是很好的想法,但是如果你向用户展示一个奶酪制作界面。他们不在乎奶酪有多少个洞或者制作奶酪的内部结构。你代码的用户只想要“gouda”或“parmesean”,对吗?

为什么不这样做:

# cheese_user.pyfrom cheeses import make_gouda, make_parmesean
gouda = make_gouda()paremesean = make_parmesean()

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

# cheeses.pyclass Cheese(object):def __init__(self, *args, **kwargs):#args -- tuple of anonymous arguments#kwargs -- dictionary of named argumentsself.num_holes = kwargs.get('num_holes',random_holes())
def make_gouda():return Cheese()
def make_paremesean():return Cheese(num_holes=15)

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

我会使用继承。特别是如果有比洞数更多的差异。特别是如果豪达需要有不同的成员集,然后是帕尔马干酪。

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

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

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

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

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

示例:

class MyClass(set):
def __init__(self, filename):self._value = load_from_file(filename)
@classmethoddef 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 initializersobj._value = load_from_somewhere(somename)return obj

这就是我必须创建的YearQuarter类的解决方法。我创建了一个__init__,它对各种输入都非常宽容。

你这样使用它:

>>> from datetime import date>>> temp1 = YearQuarter(year=2017, month=12)>>> print temp12017-Q4>>> temp2 = YearQuarter(temp1)>>> print temp22017-Q4>>> temp3 = YearQuarter((2017, 6))>>> print temp32017-Q2>>> temp4 = YearQuarter(date(2017, 1, 18))>>> print temp42017-Q1>>> temp5 = YearQuarter(year=2017, quarter = 3)>>> print temp52017-Q3

这就是__init__和类的其余部分的样子:

import datetime

class YearQuarter:
def __init__(self, *args, **kwargs):if len(args) == 1:[x]     = args
if isinstance(x, datetime.date):self._year      = int(x.year)self._quarter   = (int(x.month) + 2) / 3elif isinstance(x, tuple):year, month     = x
self._year      = int(year)
month           = int(month)
if 1 <= month <= 12:self._quarter   = (month + 2) / 3else:raise ValueError
elif isinstance(x, YearQuarter):self._year      = x._yearself._quarter   = x._quarter
elif len(args) == 2:year, month     = args
self._year      = int(year)
month           = int(month)
if 1 <= month <= 12:self._quarter   = (month + 2) / 3else:raise ValueError
elif kwargs:
self._year      = int(kwargs["year"])
if "quarter" in kwargs:quarter     = int(kwargs["quarter"])
if 1 <= quarter <= 4:self._quarter     = quarterelse:raise ValueErrorelif "month" in kwargs:month   = int(kwargs["month"])
if 1 <= month <= 12:self._quarter     = (month + 2) / 3else:raise ValueError
def __str__(self):return '{0}-Q{1}'.format(self._year, self._quarter)
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."""...
@classmethoddef make_parmesan(cls, *args, **kwargs):new = cls.__new__(cls)new._init_parmesan(*args, **kwargs)return new
@classmethoddef make_gauda(cls, *args, **kwargs):new = cls.__new__(cls)new._init_gauda(*args, **kwargs)return new

由于我最初的答案被批评根据,我的专用构造函数没有调用(唯一的)默认构造函数,我在这里发布了一个修改后的版本,以满足所有构造函数调用默认构造函数的愿望:

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."""...
@classmethoddef make_parmesan(cls, *args, **kwargs):return cls(*args, **kwargs, _initialiser="_init_parmesan")
@classmethoddef make_gouda(cls, *args, **kwargs):return cls(*args, **kwargs, _initialiser="_init_gouda")

概览

对于具体的奶酪示例,我同意许多其他关于使用默认值来发出随机初始化信号或使用静态工厂方法的答案。然而,也可能存在你想到的相关场景,在不损害参数名称或类型信息质量的情况下,拥有替代、简洁的调用构造函数的方法具有价值。

由于Python 3.8和functools.singledispatchmethod可以在许多情况下帮助实现这一点(更灵活的multimethod可以应用于更多的场景)。(这篇相关文章描述了如何在没有库的情况下在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 = aself.b = b
@__init__.registerdef _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__
  • 如果您是装饰器语法的新手,请意识到将@overload放在__init__的定义之前只是__init__ = overload(the original definition of __init__)的糖。在这种情况下,overload是一个类,因此生成的__init__是一个具有__call__方法的对象,因此它看起来像一个函数,但也有一个.register方法,稍后将调用该方法以添加另一个重载版本的__init__。这有点混乱,但它请mypy,因为没有方法名称被定义两次。如果您不关心mypy并且计划使用外部库,multimethod也有更简单的替代方法来指定重载版本。
  • 定义__repr__只是为了使打印的输出有意义(一般来说你不需要它)。
  • 请注意,multidispatch能够处理三个没有任何位置参数的附加输入组合。

我还没有看到一个简单的例子来回答。想法很简单:

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

这是一个新的尝试。

 class Person:def __init__(self, name, age):self.name = nameself.age = age
@classmethoddef fromBirthYear(cls, name, birthYear):return cls(name, date.today().year - birthYear)

用法:

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