我找不到一个明确的答案。据我所知,一个 Python 类中不能有多个 __init__
函数。那么我该如何解决这个问题呢?
假设我有一个名为 Cheese
的类,它具有 number_of_holes
属性。我怎样才能有两种创建奶酪对象的方法......
一个像这样有许多洞的洞:parmesan = Cheese(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
你说什么?还有其他方法吗?
实际上 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
如果您只需要 __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()
@classmethod
是实现多个构造函数的 Pythonic 方式。
self
的实例对象。然后是类方法(使用 @classmethod
),它们对类对象的引用为 cls
。最后还有一些静态方法(用 @staticmethod
声明),它们都没有这些引用。静态方法就像模块级别的函数,除了它们存在于类的名称空间中。
@abstractmethod
and @classmethod
on the same factory function is possible and is built into the language 的 python 3 中。我还认为这种方法更明确,与 The Zen of Python 一致。
__init__()
中有一堆 if
,而是让每个独特的工厂方法处理它们自己独特的初始化方面,并让 __init__()
只接受基本的定义实例的数据片段。例如,除了 number_of_holes
之外,Cheese
可能还有属性 volume
和 average_hole_radius
。 __init__()
将接受这三个值。然后你可以有一个类方法 with_density()
,它随机选择基本属性来匹配给定的密度,然后将它们传递给 __init__()
。
人们绝对应该更喜欢已经发布的解决方案,但是由于还没有人提到这个解决方案,我认为值得一提的是完整性。
可以修改 @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
__init__
的参数的解决方案。但是,您能否提供一些参考资料,说明这种方法以某种方式得到官方批准或支持?直接调用__new__
方法有多安全可靠?
super
否则这在协作多重继承中不起作用,因此我在您的答案中添加了该行。
如果您想使用可选参数,所有这些答案都非常好,但另一种 Pythonic 可能性是使用类方法来生成工厂风格的伪构造函数:
def __init__(self, num_holes):
# do stuff with the number
@classmethod
def fromRandom(cls):
return cls( # some-random-number )
为什么您认为您的解决方案“笨拙”?就我个人而言,在像您这样的情况下,我更喜欢一个具有默认值的构造函数,而不是多个重载的构造函数(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 子类...
这些对于您的实现来说是个好主意,但是如果您要向用户展示奶酪制作界面。他们不在乎奶酪有多少孔,也不在乎制作奶酪的内部结构。您的代码的用户只想要“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 对象,而您并不真正关心它是什么类。
make_gouda, make_parmesan
应该是 class Cheese
的类方法
概述
对于特定的奶酪示例,我同意许多其他关于使用默认值来表示随机初始化或使用静态工厂方法的答案。但是,您可能还想到了一些相关的场景,在这些场景中,在不损害参数名称或类型信息质量的情况下,使用替代、简洁的方式调用构造函数是有价值的。
由于 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)]
笔记:
我通常不会将别名称为重载,但它有助于使使用这两种方法之间的差异只是您使用哪个导入的问题。
# type: ignore[misc] 注释不是运行所必需的,但我把它们放在那里是为了取悦不喜欢装饰 __init__ 也不喜欢直接调用 __init__ 的 mypy。
如果您不熟悉装饰器语法,请意识到将 @overload 放在 __init__ 的定义之前只是 __init__ = 重载(__init__ 的原始定义)的糖。在这种情况下,重载是一个类,因此生成的 __init__ 是一个具有 __call__ 方法的对象,因此它看起来像一个函数,但也有一个 .register 方法,稍后将调用该方法以添加另一个重载版本的 __init__。这有点混乱,但它请 mypy 因为没有方法名称被定义两次。如果您不关心 mypy 并且无论如何都打算使用外部库,那么 multimethod 也有更简单的替代方法来指定重载版本。
定义 __repr__ 只是为了使打印输出有意义(您通常不需要它)。
请注意,multidispatch 能够处理三个没有任何位置参数的附加输入组合。
改为使用 num_holes=None
作为默认值。然后检查是否num_holes is None
,如果是,则随机化。无论如何,这就是我通常看到的。
更根本不同的构造方法可能需要一个返回 cls
实例的类方法。
最好的答案是上面关于默认参数的答案,但我写这篇文章很有趣,它确实符合“多个构造函数”的要求。使用风险自负。
new 方法呢?
“典型的实现通过使用带有适当参数的 super(currentclass, cls).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)
Cheese(0)
,一个使用 Cheese(1)
。线程 1 可能会运行 cls.__init__ = cls.foomethod
,但线程 2 可能会在线程 1 进一步运行之前运行 cls.__init__ = cls.barmethod
。然后两个线程最终都会调用 barmethod
,这不是您想要的。
我会使用继承。特别是如果存在比孔数更多的差异。特别是如果 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)
由于 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")
__init__
,它可以处理初始化 Cheese
而无需了解特殊种类的奶酪.其次,您定义一个类方法,该方法为某些特殊情况生成通用 __init__
的适当参数。在这里,您基本上是在重新发明部分继承。
这就是我为必须创建的 YearQuarter
类解决它的方法。我创建了一个 __init__
,它对各种输入都非常宽容。
你像这样使用它:
>>> from datetime import date
>>> temp1 = YearQuarter(year=2017, month=12)
>>> print temp1
2017-Q4
>>> temp2 = YearQuarter(temp1)
>>> print temp2
2017-Q4
>>> temp3 = YearQuarter((2017, 6))
>>> print temp3
2017-Q2
>>> temp4 = YearQuarter(date(2017, 1, 18))
>>> print temp4
2017-Q1
>>> temp5 = YearQuarter(year=2017, quarter = 3)
>>> print temp5
2017-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) / 3
elif isinstance(x, tuple):
year, month = x
self._year = int(year)
month = int(month)
if 1 <= month <= 12:
self._quarter = (month + 2) / 3
else:
raise ValueError
elif isinstance(x, YearQuarter):
self._year = x._year
self._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) / 3
else:
raise ValueError
elif kwargs:
self._year = int(kwargs["year"])
if "quarter" in kwargs:
quarter = int(kwargs["quarter"])
if 1 <= quarter <= 4:
self._quarter = quarter
else:
raise ValueError
elif "month" in kwargs:
month = int(kwargs["month"])
if 1 <= month <= 12:
self._quarter = (month + 2) / 3
else:
raise ValueError
def __str__(self):
return '{0}-Q{1}'.format(self._year, self._quarter)
__init__(self, obj)
,我使用 if str(obj.__class__.__name__) == 'NameOfMyClass': ... elif etc.
在 __init__
内进行测试。
__init__
应该直接占用一年零一个季度,而不是一个未知类型的值。类方法 from_date
可以处理从 datetime.date
值中提取年份和季度,然后调用 YearQuarter(y, q)
。您可以定义一个类似的类方法 from_tuple
,但这似乎不值得这样做,因为您可以简单地调用 YearQuarter(*t)
。
__init__
不应负责分析您可能用于创建实例的每组可能的值。 def __init__(self, year, quarter): self._year = year; self._quarter = quarter
:就是这样(尽管可能需要对 quarter
进行一些范围检查)。其他类方法处理将一个或多个不同参数映射到可以传递给 __init__
的年份和季度的工作。
from_year_month
需要一个月 m
,将其映射到一个季度 q
,然后调用 YearQuarter(y, q)
。 from_date
从 date
实例中提取年份和月份,然后调用 YearQuarter._from_year_month
。没有重复,每个方法负责一种特定的方式来生成传递给 __init__
的年份和季度。
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
__init__
方法,而其他类方法要么按原样调用它(最干净),要么通过您需要的任何辅助类方法和设置器(理想情况下没有)处理特殊的初始化操作。
__init__
方法。我不明白为什么有人会想要它。 “其他类方法要么按原样调用它”——调用什么? __init__
方法?明确地调用 __init__
IMO 会很奇怪。
_init...
方法中(请参阅此问题的其他答案。)更糟糕的是,在这种情况下您甚至不需要:您没有'没有显示 _init_parmesan, _init_gouda
的代码有何不同,因此没有理由不将它们通用化。无论如何,这样做的 Pythonic 方法是向 *args 或 **kwargs 提供非默认参数(例如 Cheese(..., type='gouda'...)
,或者如果它不能处理所有事情,则将通用代码放在 __init__
和不太常见的- 在类方法 make_whatever...
中使用了代码并使用它校准设置器
__init__
内的某些(可能很尴尬)调度使用单个默认构造函数实现,如果例程完全独立,我将调用它们 _init_from_foo
、_init_from_bar
等,并从__init__
在由 isinstance
或其他测试分派后。
我还没有看到一个简单的例子。这个想法很简单:
使用 __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)
kwargs
代表 关键字参数(一旦你知道它似乎就是逻辑)。 :)*args
和**kwargs
是矫枉过正的。在大多数构造函数中,您想知道您的参数是什么。