在 __new__ 中进行初始化工作是“原罪”吗?如果是这样,如何最好地处理单例的运行一次初始化?

Posted

技术标签:

【中文标题】在 __new__ 中进行初始化工作是“原罪”吗?如果是这样,如何最好地处理单例的运行一次初始化?【英文标题】:Is it an 'original sin' to do init work in __new__? If so, how best to handle run once init for singletons? 【发布时间】:2012-03-21 05:04:15 【问题描述】:

我有一个具有我使用的标准 python 单例形式的类。它还初始化一个值,但不是在每个实例中都初始化它,而是在我进行单例处理时在 __new__() 中初始化它。这是pythonic的方式吗?如果没有,我还能怎么办?

class Foo(object):
    _inst = None
    def __new__(cls, val):
        if Foo._inst is None:
            Foo._inst = super(Foo, self).__new__(cls)
            Foo._inst.val = val
        return Foo._inst

这显然是简化的形式。我已经使用了等待__init__() 的构造来检查并设置Foo._inst 来处理单例的运行一次初始化,但这对于多线程情况下的同步错误似乎已经成熟。如果再次运行__init__(),则在现有实例中没有可能损坏的值的情况下,将 init 放入__init__() 并没有什么害处,除非您正在运行的代码在您不这样做时实际上什么都不做不需要,但我不想覆盖通常会损坏的状态值。我知道单例也是邪恶的,但有时,它们是最好的解决方案(比如定义特殊用途类型,我希望能够通过 is 进行检查)。

这似乎是我能想到的最佳解决方案,但我觉得在 __new__() 中初始化实例值很脏。有没有人知道如何更好地处理这个问题?

【问题讨论】:

使用单例是原罪。它们是一种为全局变量添加额外缺点和奇怪限制的方法。如果您需要全局状态,请使用全局变量,但最好尽量避免使用它们。永远不要使用单例。 @Sven Marnach 当然,我尽我所能避免单例和全局,但在某些情况下它们是最好的解决方案。例如,我刚刚实现了一个三元类,但我不需要每种类型的多个实例(True、False、Unknown),实际上,只有一个实例让我可以像使用布尔值一样使用它,因为它允许is 运营商工作。因此,虽然要避免单例,但当它们是最佳选择时,应该有很好的方法来使用它们。 你不是说“大罪”吗? 使用博格。 (它看起来像一个普通的类,但实例字典存储在类上,所以每个实例实际上都一样。) 另外你不应该在布尔值上使用is。例如,[] is not False 但空列表为布尔值 false。 【参考方案1】:

您始终可以使用全局变量而不是单例。具有三个不同实例的类的示例:

class _Ternary(object):
    """Internal class.  Don't instantiate!"""
    def __init__(self, value):
        self.value = value

ternary_0 = _Ternary(0)
ternary_1 = _Ternary(1)
ternary_2 = _Ternary(2)

def ternary(value):
    """Factory function to request a "ternary" instance."""
    return 0: ternary_0, 1: ternary_1: 2: ternary_2[value]

这是直截了当的,可以避免单身人士的痛苦。

【讨论】:

但是使用这种模式如何使操作符重载工作得很好呢?例如,如何让ternary_1 - ternary_1 返回ternary_0 @GrantJ:我不明白这个问题。在Ternary 类上定义__sub__ 方法?【参考方案2】:

回答第一个问题: 单例在 Python 中并不经常使用,因为它们并不是真正需要的。

回答第二个: 如果你认为你真的需要它们,有时会使用一种更简洁、类似的模式,称为“Borg”,请参阅:the Borg design pattern 如果您只需要在实例之间共享一些属性并且不需要成熟的单例(可以实例化一次),您可以简单地使用类属性,如下所示:

class Foo( object ):
    val = ...

    def __init__( self ):
        ...

那么所有实例将共享相同的“val”属性值。

【讨论】:

我的例子可能过于简单了......在更常见的例子中,我实际上在类中存储了一个实例字典,我的目标是限制特殊类型实例的数量。所以你最终得到Foo._insts = 1 : Foo(1), 2 : Foo(2), 3 : Foo(3)。然后,您最终会得到 3 个值的唯一类型,但每个值只有一个实例。 Python 不是一种允许任何如此严格的语言,即无论如何你可能无法 100% 保护自己。但是,如果您只是想为此创建一个像样的接口,为什么不创建两个类,其中一个是寄存器,如果没有超过限制,则允许创建其他的实例?即 Foo() 和 FooRegistry() 并有一个方法 FooRegistry.create() 如果没有超过限制,则返回 Foo 实例,否则会引发异常,或者做其他事情来处理这种情况......? 为什么要添加另一层类来做同样的事情?这会将 FooRegistery 和 Foo 折叠到一个类中,其中 FooRegistery.create() 变成 Foo.__new__()。 因为我们认为它不太优雅和 pythonic,请参阅@Sven Marnach 对您的问题的评论。此外,这些类应该映射到您的问题域中的某种逻辑对象。有了你所说的,你显然在这里有两个对象 - Foo,以及某种跟踪 Foos 的注册表,那么为什么不让它变得简单(KISS 原则)和干净呢? @sr2222:如果你认为你的例子过于简单,你可以edit你的问题并更新它/改进它/添加更多有用的信息。在那里说明您的要求和实际代码,这样我们就可以真正帮助您/告诉您是否有问题。【参考方案3】:

没有更好的方法来创建单例。如果你打算这样做(三值逻辑是我能想到的唯一一个有意义的例子),那么你可以按照你的方式去做。

【讨论】:

是的,这就是我使用 Python Tribool 数据类型的用例。

以上是关于在 __new__ 中进行初始化工作是“原罪”吗?如果是这样,如何最好地处理单例的运行一次初始化?的主要内容,如果未能解决你的问题,请参考以下文章

Python中的__init__和__new__介绍

初始化器与构造器[重复]

魔术方法

JS可否 var a[id] = new init();创建对象数组、进行赋值?

详解Python中的__new__、__init__、__call__三个特殊方法

007_Python中的__init__,__call__,__new__