Python中有多个构造函数的干净,pythonic方法是什么?
我找不到一个明确的答案。 AFAIK,在Python类中不能有多个__init__
函数。 那么解决这个问题的好方法是什么?
假设我有一个叫做Cheese
的类,它有number_of_holes
属性。 我怎么能有两种方法来创build奶酪对象…
- 一个需要像这样的一些洞:
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}
使用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))
现在创build这样的对象:
gouda = Cheese() emmentaler = Cheese.random() leerdammer = Cheese.slightly_holey()
所有这些答案都非常好,如果你想使用可选参数,但另一种Pythonic的可能性是使用类方法来生成一个工厂风格的伪构造函数:
def __init__(self, num_holes): # do stuff with the number @classmethod def fromRandom(cls): return cls( # some-random-number )
你为什么认为你的解决scheme“笨拙”? 就个人而言,我喜欢一个构造函数的默认值超过多个重载的构造函数在你情况下(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子类,但是…
这些对你的实现来说是个好主意,但是如果你正在向用户展示一个奶酪制作界面。 他们不在乎奶酪有多less个孔,或者内部制造奶酪的东西。 你的代码的用户只是想要“高达”或“parmesean”是正确的?
那么为什么不这样做:
# cheese_user.py from cheeses import make_gouda, make_parmesean gouda = make_gouda() paremesean = make_parmesean()
然后你可以使用上面的任何方法来实际的实现这些function:
# 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对象,你并不在乎它是什么类。
最好的答案是关于默认论点的上面的答案,但是我很乐意写这个,它确实适合“多重构造函数”的账单。 使用风险自负。
那么新方法呢?
“典型的实现是通过使用super(currentclass,cls).new(cls [,…])和适当的参数调用超类的new ()方法,然后根据需要修改新创build的实例来创build类的新实例归还“。
所以你可以通过附加适当的构造函数方法来让新方法修改你的类定义。
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)
num_holes=None
使用num_holes=None
作为默认值。 然后检查num_holes is None
是否num_holes is None
,如果是,则随机化。 无论如何,这就是我通常所看到的。
更完全不同的构造方法可能需要返回cls
实例的分类方法。
一个人应该更喜欢已经发布的解决scheme,但由于没有人提到这个解决scheme,我认为这是值得一提的完整性。
可以修改@classmethod
方法,以提供不调用默认构造函数( __init__
)的替代构造函数。 相反,一个实例是使用__new__
创build的。
如果根据构造函数参数的types不能select初始化的types,则构造函数不能共享代码。
例:
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
我会使用inheritance。 特别是如果有更多的差异比洞数。 特别是如果高达将需要有不同的成员,然后巴马。
class Gouda(Cheese): def __init__(self): super(Gouda).__init__(num_holes=10) class Parmesan(Cheese): def __init__(self): super(Parmesan).__init__(num_holes=15)
来自Alex Martelli的文字
(如@ariddell的评论所述 )
好的,我已经研究并阅读了关于如何模拟多个构造函数的文章(和FAQ)。 Alex Martelli通过testing* args的数量并执行适当的代码段提供了最可靠的答案。 但在另一篇文章中他说
基于给定的参数有多less,这会“重载”构造函数 – 这是多么优雅(以及Pythonic是怎样的!),这当然是有争议的 。 在types上重载会更不优雅,更lessPythonic,尽pipe你可以很容易地扩展这个想法来做到这一点 – 我会更加强烈地阻止它。
我认为我的回应的这一部分是什么使它“健壮”:-)。
换句话说,这种查询的最佳答案是:你可以做到这一点(这里是怎么做的),但有更好的方法(在这里)。 诚然,我并没有完全进入“这里是如何”和“他们在这里”的部分。
然而,我需要做的是什么是沮丧,即创build3个构造函数都带有2个参数,其中每个的第二个参数是不同的types。 真正的踢球者是在其中一个构造函数中,我需要检查对象的类以确保该方法正在接收适当的对象。 我没有问题,如果这是它的方式,但如果有更多的可接受(和Pythonic)的方式来做到这一点,我将不胜感激一些指针。
为什么你认为你需要根据论证的types或者类来区分你的处理? 更有可能的是,你想了解一个参数(在不同的情况下确定不同的处理), 它是如何实现的 – 你不能通过testingtypes或类来做到这一点。 相反,你可以使用hasattr或尝试/除了找出。
通过专注于行为而不是types标识,您可以使客户端代码编程人员更轻松:他或她可以使用任何实现所需行为的实例来对您的组件进行多态使用,这些都是您的代码所需要的。
“重载”的概念 – 具有一个单一给定名称的可调用,根据不同的条件映射到多个内部可调用 – 也与多态有关; 提供映射到多个请求的单个可调用对象的唯一好理由是让客户端代码在需要时使用单个可调用对象。
直接暴露多个可以调用的函数通常是一个好主意 – 不要让客户机代码程序员通过奇怪的扭曲来确保最终调用“正确”的重载; 当他们的需求是非多态时,让他们以最简单的方式尽可能多地陈述。 这与“构造函数”并不一致,这就是为什么在任何应用程序需要一些丰富性和复杂性的情况下, 工厂函数往往是可取的(不是函数的工厂可调用函数也可以,但是遇到一个比较罕见的,更多参与需求)。
在Python中(就像在VB中一样,其他语言也有显式命名和默认值参数的概念),你可以使用另一种“多个可调用”的文体替代方法:通过提供不同的named-参数。 这很容易被过度使用(而且VB提供了很多这种风格被滥用的例子) – 但是,在品味和适度的情况下使用它也是非常有用的。
我们来看一个典型的例子。 我们想揭露一个class级的芒格,他们的实例需要用“大量的数据进行初始化”。 “大量的数据”可能是一个文件,一个string,或者是我们自己的类DataBuffer的一个实例,它提供了Munger实例所需要的数据访问特性 – 实际上,当我们获得一个文件或string时,我们构造一个DataBuffer反正我们自己也是这样。
“过载”风格可能是:
class Munger: def __init__(self, data): name = type(data).__name__ if name=='instance': name = data.__class__.__name__ method = getattr(self, '_init_'+name) method(data) def _init_string(self, data): self.data = DataBuffer(data) def _init_file(self, data): self.data = DataBuffer(data) def _init_DataBuffer(self, data): self.data = data
现在,这是一个“不好的例子”,也许我已经做了不好的事情,但是我希望至less可以明白,为什么这样做会很不理想。 这不会以任何方式利用DataBuffer自身构造函数的多态性,严格地禁止客户机代码的多态性能力(除非通过诸如命名类的技巧,例如'string'…!)。
这样做的结构显然比较简单,因为“一个Munger需要传递一个DataBuffer,或者一个DataBuffer可以从其中构build:”
class Munger: def __init__(self, data): if not isinstance(data, DataBuffer): data = DataBuffer(data) self.data = data
至less,我们在这里有一些简单。 虽然多态性仍然不是最佳的; 如果客户端代码想要模仿一个数据缓冲区,那么即使它没有使用它的任何实现,它也需要inheritance我们的DataBuffer类,只是为了满足我们的isinstance检查。 至less,DataBuffer会从接口和实现部分“拆分”出来:
class IDataBuffer: def rewind(self): raise TypeError, "must override .rewind method" def nextBytes(self, N): raise TypeError, "must override .nextBytes method" def pushBack(self, bytes): raise TypeError, "must override .pushBack method"
等等,类DataBufferinheritance此(并提供所需的覆盖,当然)和isata检查完成对IDataBuffer。 Pythonic不是很好,但是如果有很多DataBuffer方法是可行的 – 分别检查它们可能会变得比它的价值更麻烦。
DataBuffer自己的“重载”(“我正在从文件或string初始化?”)需要处理。 再一次,编码是严重错误的:
class DataBuffer(IDataBuffer): def __init__(self, data): name = type(data).__name__ if name=='instance': name = data.__class__.__name__ method = getattr(self, '_init_'+name) method(data) def _init_string(self, data): self.data = data self.index = 0 def _init_file(self, data): self.data = data.read() self.index = 0 # etc etc
因为它大大地抑制了客户代码的多态性。 在这里,我们需要的一个'文件对象'是一个.read方法,我们可以调用没有参数来提供我们的数据 – 那么为什么不直接编码…:
class DataBuffer(IDataBuffer): def __init__(self, data): try: self.data = data.read() except AttributeError: self.data=data self.index = 0 # etc etc
当然,这简单得多。 可以在初始化时添加一些testing,以确保结果数据可用于我们的目的,但是如果错误(如果有的话)在第一次使用而不是初始化时通常不是大问题。
另一种架构也值得考虑。 DOES客户端代码是否真的需要传递一个Munger构造函数时隐含的多态性,一个文件(类似)对象还是一个类似string的types,它们的隐含语义非常不同,关于如何从所述对象获取数据? Python库为我们提供了反例 – 类文件对象和类似string的对象通常通过单独的方法传递; 那里没有真正的多态性机会!
所以…:
class DataBuffer(IDataBuffer): def __init__(self, data): self.data = data self.index = 0 # etc etc class Munger: def __init__(self, data): self.data = data # etc etc def FileMunger(afile): return Munger(DataBuffer(afile.read())) def StringMunger(astring): return Munger(DataBuffer(astring))
在那里,这不是更好吗? 两个非重载的工厂函数,在构造函数中最大的简单性。
客户端代码知道它是用来构造Munger的,它不需要多态性 – 如果它适当地调用FileMunger或StringMunger,它将会更清晰,更明确和可读,并且只需要使用Munger的ctor就可以满足需要重用一些现有的IDataBuffer实例。
如果偶尔使用多态可能会使客户端代码作者受益,那么我们可以仅为此添加其他工厂函数:
def AnyMunger(mystery): if isinstance(mystery, IDataBuffer): return Munger(mystery) else: try: return FileMunger(mystery) except AttributeError: return StringMunger(mystery)
然而, 除非某种特定的用例/场景清楚地显示它的适当性,否则不会只是增加这些东西 – “你不需要它”是一个伟大的devise原则:-) [XP规则.. ! – )]。
现在,这当然是一个玩具级的例子,但是我希望正因为如此,它可能会更清楚地显示出问题 – 也许可以说服你以更简单,更实用的方式重新考虑你的devise。
亚历克斯
这就是我为了创build一个YearQuarter
类而解决的问题。 我用一个叫做value
参数创build了一个__init__
。 __init__
的代码只是决定什么types的value
参数,并相应地处理数据。 如果你想要多个input参数,你可以将它们打包到一个元组中,然后testingvalue
是一个元组。
你这样使用它:
>>> 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__
。 我省略了这个例子。