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

win*_*ith 660 python constructor

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

假设我有一个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_holes
        else:
            number_of_holes = num_holes
Run Code Online (Sandbox Code Playgroud)

你说什么?还有另外一种方法吗?

var*_*tec 780

实际上None对于"神奇"的价值要好得多:

class Cheese():
    def __init__(self, num_holes = None):
        if num_holes is None:
            ...
Run Code Online (Sandbox Code Playgroud)

现在,如果您想完全自由地添加更多参数:

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())
Run Code Online (Sandbox Code Playgroud)

为了更好地解释*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}
Run Code Online (Sandbox Code Playgroud)

http://docs.python.org/reference/expressions.html#calls

  • 对于那些感兴趣的人,`kwargs`代表*关键字参数*(一旦你知道它就显得逻辑).:) (51认同)
  • 对于 2020 年来自 google 的人们,请向下滚动一下此页面 - 再往下看“Ber”的答案是可靠的,并且在大多数情况下比这条路线更加Pythonic。 (13认同)
  • 有些时候,`* args`和`** kwargs`可能会过大。在大多数构造函数中,您想知道您的参数是什么。 (8认同)
  • @user989762是的,这种方法根本不是自我记录的(您有多少次尝试使用库并尝试从方法签名中直觉使用,却发现您必须进行代码潜水才能查看预期的参数/允许吗?)此外,现在您的实现承担了参数检查的额外负担,包括选择是否接受或排除(teehee)不支持的参数。 (7认同)

Ber*_*Ber 631

num_holes=None如果你想要的话,使用默认是很好的__init__.

如果您需要多个独立的"构造函数",则可以将它们作为类方法提供.这些通常称为工厂方法.在这种情况下,你可以有默认num_holesBE 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))
Run Code Online (Sandbox Code Playgroud)

现在创建这样的对象:

gouda = Cheese()
emmentaler = Cheese.random()
leerdammer = Cheese.slightly_holey()
Run Code Online (Sandbox Code Playgroud)

  • 我认为这也更清洁.这是一个更清晰的解释的链接,恕我直言 - 来自comp.lang.python帖子,Alex Martelli的"Re:Multiple constructors".http://coding.derkeiler.com/Archive/Python/comp.lang.python/2005-02/1294.html (27认同)
  • @rmbianchi:接受的答案可能更符合其他语言,但它也不那么pythonic:`@ classmethod`s是实现多个contstructors的pythonic方式. (19认同)
  • @Bepetersn有一些实例方法(普通方法),它们的实例对象被称为`self`.然后是类方法(使用`@ classmethod`),它们将类对象引用为`cls`.最后有一些静态方法(用`@ staticmethod`声明),它们都没有这些引用.静态方法就像模块级别的函数一样,除了它们存在于类的名称空间中. (11认同)
  • 与公认的解决方案相比,此方法的优点是它可以轻松地允许指定抽象构造函数并强制执行它们,特别是对于 python 3,其中[可以在同一工厂函数上使用`@abstractmethod`和`@classmethod`并内置于语言中](https://docs.python.org/3.4/library/abc.html#abc.abstractclassmethod)。我还认为这种方法更加明确,这与[Python之禅](http://legacy.python.org/dev/peps/pep-0020/)相一致。 (5认同)
  • @RegisMay (1/2) 与其在 `__init__()` 中使用一堆 `if`,技巧是让每个独特的工厂方法处理它们自己独特的初始化方面,并让 `__init__()`仅接受定义实例的基本数据片段。例如,除了“number_of_holes”之外,“Cheese”可能还具有属性“volume”和“average_hole_radius”。`__init__()` 将接受这三个值。然后你可以有一个类方法“with_densis()”,它随机选择基本属性来匹配给定的密度,随后将它们传递给“__init__()”。 (3认同)
  • @ashu 其他构造函数通过 cls(...) 实例化类来调用 __init__() 方法。因此,number_of_holes 始终以相同的方式使用。 (2认同)

Yes*_*ke. 24

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

def __init__(self, num_holes):

  # do stuff with the number

@classmethod
def fromRandom(cls):

  return cls( # some-random-number )
Run Code Online (Sandbox Code Playgroud)


Fer*_*yer 18

为什么你认为你的解决方案"笨重"?就个人而言,我个人更喜欢一个默认值超过多个重载构造函数的构造函数(Python不支持方法重载):

def __init__(self, num_holes=None):
    if num_holes is None:
        # Construct a gouda
    else:
        # custom cheese
    # common initialization
Run Code Online (Sandbox Code Playgroud)

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

@classmethod
def create_gouda(cls):
    c = Cheese()
    # ...
    return c

@classmethod
def create_cheddar(cls):
    # ...
Run Code Online (Sandbox Code Playgroud)

在您的奶酪示例中,您可能想要使用奶酪的Gouda子类...

  • 工厂函数使用_cls_:使用_cls_而不是_Cheese_.如果没有,使用类方法而不是静态方法有什么意义? (3认同)

Bra*_*d C 17

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

那么为什么不这样做:

# cheese_user.py
from cheeses import make_gouda, make_parmesean

gouda = make_gouda()
paremesean = make_parmesean()
Run Code Online (Sandbox Code Playgroud)

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

# 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)
Run Code Online (Sandbox Code Playgroud)

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

  • `make_gouda,make_parmesan`应该是`class Cheese`的classmethods (3认同)
  • 我倾向于选择这种方法,因为它与 [工厂方法模式](http://en.wikipedia.org/wiki/Factory_method_pattern) 非常相似。 (2认同)

And*_*bis 16

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

@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
Run Code Online (Sandbox Code Playgroud)

  • 这是确实提供独立构造函数而不是摆弄`__init__` 参数的解决方案。但是,您能否提供一些参考资料,说明此方法以某种方式得到官方批准或支持?直接调用`__new__`方法安全可靠吗? (9认同)

use*_*389 15

概述

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

从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 = 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),
])
Run Code Online (Sandbox Code Playgroud)

输出:

[(1, test), (1, test), (0, test), (1, test), (1, test), (0, test), (1, default)]
Run Code Online (Sandbox Code Playgroud)

笔记:

  • 我通常不会将别名称为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能够处理三个没有任何位置参数的附加输入组合。


mlu*_*bke 10

最好的答案就是上面关于默认参数的答案,但我写这篇文章很有意思,它确实符合"多个构造函数"的要求.使用风险由您自己承担.

方法怎么样?

"典型的实现通过调用超类的创建类的新实例使用超级(currentclass,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.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)
Run Code Online (Sandbox Code Playgroud)

  • 这种代码让我做了关于使用动态语言进行噩梦的代码 - 并不是说​​它存在任何本质上的错误,只是它违反了我对类的一些关键假设. (10认同)
  • @ Reti43说两个线程试图同时创建奶酪,一个用'Cheese(0)`和一个用`Cheese(1)`.线程1可能会运行`cls .__ init__ = cls.foomethod`,但是线程2可能会在线程1进一步运行之前运行`cls .__ init__ = cls.barmethod`.然后两个线程最终都会调用`barmethod`,这不是你想要的. (8认同)

Dev*_*rre 8

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

更完全不同的构造方法可能需要一种返回实例的类方法cls.