ChatGPT解决这个技术问题 Extra ChatGPT

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

我找不到一个明确的答案。据我所知,一个 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

你说什么?还有其他方法吗?

我认为 init 不是构造函数,它是初始化程序。 new 将是一个构造函数

E
Elias Zamaria

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


对于那些感兴趣的人,kwargs 代表 关键字参数(一旦你知道它似乎就是逻辑)。 :)
有些时候 *args**kwargs 是矫枉过正的。在大多数构造函数中,您想知道您的参数是什么。
@user989762 是的!当然!
@user989762 是的,这种方法根本不是自我记录的(您有多少次尝试使用库并尝试从方法签名中直观地了解用法,结果却发现您必须进行代码潜水以查看预期的参数/允许?)此外,现在您的实现承担了参数检查的额外负担,包括选择是接受还是排除(teehee)不受支持的参数。
对于 2020 年来自谷歌的人来说,向下滚动一下这个页面——在大多数情况下,“Ber”的答案比这条路线更可靠,更符合 Python 风格。
S
ShadowRanger

如果您只需要 __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()

@rmbianchi:接受的答案可能更符合其他语言,但它也不那么 Pythonic:@classmethod 是实现多个构造函数的 Pythonic 方式。
@Bepetersn 有实例方法(普通方法),它们有一个引用为 self 的实例对象。然后是类方法(使用 @classmethod),它们对类对象的引用为 cls。最后还有一些静态方法(用 @staticmethod 声明),它们都没有这些引用。静态方法就像模块级别的函数,除了它们存在于类的名称空间中。
这种方法相对于公认的解决方案的一个优点是它可以轻松地指定抽象构造函数并强制执行它们,尤其是在其中 the usage of @abstractmethod and @classmethod on the same factory function is possible and is built into the language 的 python 3 中。我还认为这种方法更明确,与 The Zen of Python 一致。
@ashu 其他构造函数通过 cls(...) 实例化类来调用 __init__() 方法。因此,number_of_holes 总是以相同的方式使用。
@RegisMay (1/2) 不是在 __init__() 中有一堆 if,而是让每个独特的工厂方法处理它们自己独特的初始化方面,并让 __init__() 只接受基本的定义实例的数据片段。例如,除了 number_of_holes 之外,Cheese 可能还有属性 volumeaverage_hole_radius__init__() 将接受这三个值。然后你可以有一个类方法 with_density(),它随机选择基本属性来匹配给定的密度,然后将它们传递给 __init__()
N
Neil G

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

可以修改 @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 否则这在协作多重继承中不起作用,因此我在您的答案中添加了该行。
我想知道是否可以定义一个装饰器“构造器”(包装新的和超级的东西)然后做:@constructor def other_init(self, stuff): self.stuff = stuff
Y
Yes - that Jake.

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

def __init__(self, num_holes):

  # do stuff with the number

@classmethod
def fromRandom(cls):

  return cls( # some-random-number )

F
Ferdinand Beyer

为什么您认为您的解决方案“笨拙”?就我个人而言,在像您这样的情况下,我更喜欢一个具有默认值的构造函数,而不是多个重载的构造函数(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 子类...


工厂函数使用 cls:使用 cls 代替 Cheese。如果不是,那么使用类方法而不是静态方法有什么意义?
B
Brad C

这些对于您的实现来说是个好主意,但是如果您要向用户展示奶酪制作界面。他们不在乎奶酪有多少孔,也不在乎制作奶酪的内部结构。您的代码的用户只想要“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 对象,而您并不真正关心它是什么类。


我倾向于选择这种方法,因为它与 Factory Method pattern 非常相似。
make_gouda, make_parmesan 应该是 class Cheese 的类方法
t
teichert

概述

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

由于 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 能够处理三个没有任何位置参数的附加输入组合。


感谢您的回答和对多方法包的引用。在某些情况下,多次调度感觉很自然。在 Julia 工作了一段时间,这是我在 Python 中想念的东西。
D
Devin Jeanpierre

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

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


“classmethod”是字面的吗?还是您的意思是class method
m
mluebke

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

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)

这种代码让我对使用动态语言做噩梦——并不是说它有任何内在的错误,只是它违反了我对一个类所做的一些关键假设。
@javawizard 是否容易在评论中解释是什么使它成为非线程安全的,或者给出一个指针以便我可以在其他地方阅读它?
@Reti43 假设两个线程同时尝试创建奶酪,一个使用 Cheese(0),一个使用 Cheese(1)。线程 1 可能会运行 cls.__init__ = cls.foomethod,但线程 2 可能会在线程 1 进一步运行之前运行 cls.__init__ = cls.barmethod。然后两个线程最终都会调用 barmethod,这不是您想要的。
实际上,没有理由仅仅为了处理类的一个实例的创建而修改类的定义。
M
Michel Samia

我会使用继承。特别是如果存在比孔数更多的差异。特别是如果 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) 

继承可能是合适的,但它确实是一个与所要求的内容正交的问题。
P
PiCTo

由于 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")

类方法的想法是将创建一个特殊实例分成两个独立的部分:首先,您定义一个 generic __init__,它可以处理初始化 Cheese 而无需了解特殊种类的奶酪.其次,您定义一个类方法,该方法为某些特殊情况生成通用 __init__ 的适当参数。在这里,您基本上是在重新发明部分继承。
E
Elmex80s

这就是我为必须创建的 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)

我已经有效地使用了它,但使用了我自己的类而不是 Python 类型。给定 __init__(self, obj),我使用 if str(obj.__class__.__name__) == 'NameOfMyClass': ... elif etc.__init__ 内进行测试。
这真的不是很 Pythonic。 __init__ 应该直接占用一年零一个季度,而不是一个未知类型的值。类方法 from_date 可以处理从 datetime.date 值中提取年份和季度,然后调用 YearQuarter(y, q)。您可以定义一个类似的类方法 from_tuple,但这似乎不值得这样做,因为您可以简单地调用 YearQuarter(*t)
@chepner 我给了它一个巨大的更新。请告诉我你的想法。
特殊情况仍然是一团糟(甚至比以前更糟)。 __init__ 不应负责分析您可能用于创建实例的每组可能的值。 def __init__(self, year, quarter): self._year = year; self._quarter = quarter:就是这样(尽管可能需要对 quarter 进行一些范围检查)。其他类方法处理将一个或多个不同参数映射到可以传递给 __init__ 的年份和季度的工作。
例如,from_year_month 需要一个月 m,将其映射到一个季度 q,然后调用 YearQuarter(y, q)from_datedate 实例中提取年份和月份,然后调用 YearQuarter._from_year_month。没有重复,每个方法负责一种特定的方式来生成传递给 __init__ 的年份和季度。
A
Alexey
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

不,这完全不是 Pythonic,就像 Java 伪装成 Python 语法一样。您需要一个 __init__ 方法,而其他类方法要么按原样调用它(最干净),要么通过您需要的任何辅助类方法和设置器(理想情况下没有)处理特殊的初始化操作。
当我有多个具有不同初始化例程的构造函数时,我不想要单个 __init__ 方法。我不明白为什么有人会想要它。 “其他类方法要么按原样调用它”——调用什么? __init__ 方法?明确地调用 __init__ IMO 会很奇怪。
Alexey,拥有多个构造函数是完全不符合 Python 的,就像在多个 _init... 方法中(请参阅此问题的其他答案。)更糟糕的是,在这种情况下您甚至不需要:您没有'没有显示 _init_parmesan, _init_gouda 的代码有何不同,因此没有理由不将它们通用化。无论如何,这样做的 Pythonic 方法是向 *args 或 **kwargs 提供非默认参数(例如 Cheese(..., type='gouda'...),或者如果它不能处理所有事情,则将通用代码放在 __init__ 和不太常见的- 在类方法 make_whatever... 中使用了代码并使用它校准设置器
“拥有多个构造函数是完全不符合 Python 的”——最初的问题仍然是“在 Python 中拥有多个构造函数的干净、 Python 的方式是什么?”。我只展示了如何拥有它们,而不是为什么我想要它们。
即使多个初始化例程可以通过 __init__ 内的某些(可能很尴尬)调度使用单个默认构造函数实现,如果例程完全独立,我将调用它们 _init_from_foo_init_from_bar 等,并从__init__ 在由 isinstance 或其他测试分派后。
T
Tim C.

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

使用 __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)