如何创建一个不允许重复实例的类(在可能的情况下返回现有实例)?

Posted

技术标签:

【中文标题】如何创建一个不允许重复实例的类(在可能的情况下返回现有实例)?【英文标题】:How to make a class which disallows duplicate instances (returning an existing instance where possible)? 【发布时间】:2018-11-25 19:00:15 【问题描述】:

我有数据,每个条目都需要是一个类的实例。我希望在我的数据中遇到许多重复的条目。我本质上希望得到一组所有唯一条目(即丢弃任何重复项)。但是,实例化整个批次并在事后将它们放入一个集合中并不是最优的,因为...

    我有 很多 个条目, 预计重复条目的比例会相当高, 我的__init__() 方法对每个唯一条目进行了大量昂贵的计算,因此我想避免不必要地重做这些计算。

我知道这与here 提出的问题基本相同,但是...

    接受的答案实际上并不能解决问题。如果您让__new__() 返回一个现有实例,从技术上讲,它不会创建一个新实例,但它仍然调用__init__(),然后重做您已经完成的所有工作,这使得覆盖__new__() 完全没有意义。 (这很容易通过在 __new__()__init__() 中插入 print 语句来演示,这样您就可以看到它们何时运行。)

    当您想要一个新实例时,另一个答案需要调用类方法而不是调用类本身(例如:x = MyClass.make_new() 而不是x = MyClass())。这可行,但恕我直言,这不是理想的做法,因为这不是创建新实例的正常方式。

__new__() 是否可以被覆盖,以便返回一个现有的实体而不再次在其上运行__init__()?如果这不可能,是否有其他方法可以解决这个问题?

【问题讨论】:

是什么让你说“创建一个新实例不是正常的做法。”?在 Python 中,使用类方法作为构造函数是一种非常标准的做法。 MyClass.make_new() 绝对是要走的路。让MyClass() 返回一个现有对象对于阅读代码的开发人员来说一点也不明显。避免出人意料的行为。这是 C++ 程序员有时会陷入的陷阱:在重载运算符后面隐藏陷阱。 看来你要创建一个单例类:***.com/questions/6760685/… 注意,通常你可以使用元类 __call__ 方法:***.com/questions/6966772/… @MadPhysicist 在我组织的代码中没有构造函数,在我参加的几个 Python MOOC 中也没有教过我。这就是我这么说的原因。但我可能会弄错,因为我的经验有限。 【参考方案1】:

假设您有识别重复实例的方法以及此类实例的映射,您有几个可行的选择:

    使用classmethod 为您获取实例。 classmethod 的用途与元类中的__call__ 相似(当前为type)。主要区别在于它会在调用__new__之前检查具有请求密钥的实例是否已经存在:

    class QuasiSingleton:
        @classmethod
        def make_key(cls, *args, **kwargs):
            # Creates a hashable instance key from initialization parameters
    
        @classmethod
        def get_instance(cls, *args, **kwargs):
            key = cls.make_key(*args, **kwargs)
            if not hasattr(cls, 'instances'):
                cls.instances = 
            if key in cls.instances:
                return cls.instances[key]
            # Only call __init__ as a last resort
            inst = cls(*args, **kwargs)
            cls.instances[key] = inst
            return inst
    

    我会推荐使用这个基类,特别是如果你的类在任何方面都是可变的。您不希望一个实例的修改出现在另一个实例中,而没有明确说明这些实例可能相同。执行cls(*args, **kwargs) 意味着您每次都获得不同的实例,或者至少您的实例是不可变的并且您不在乎。

    在您的元类中重新定义 __call__

    class QuasiSingletonMeta(type):
        def make_key(cls, *args, **kwargs):
            ...
    
        def __call__(cls, *args, **kwargs):
            key = cls.make_key(*args, **kwargs)
            if not hasattr(cls, 'instances'):
                cls.instances = 
            if key in cls.instances:
                return cls.instances[key]
            inst = super().__call__(*args, **kwargs)
            cls.instances[key] = inst
            return inst
    

    这里,super().__call__ 相当于为cls 调用__new____init__

在这两种情况下,基本的缓存代码是相同的。主要区别在于如何从用户的角度获取新实例。使用像get_instance 这样的classmethod 可以直观地通知用户他们正在获取重复的实例。使用对类对象的正常调用意味着实例将始终是新的,因此应该只对不可变类这样做。

请注意,在上述两种情况下,在没有__init__ 的情况下调用__new__ 并没有多大意义。

    第三种混合选项是可能的。使用此选项,您将创建一个新实例,但从现有实例复制__init__ 计算的昂贵部分,而不是重新进行。如果通过元类实现,这个版本不会引起任何问题,因为所有实例实际上都是独立的:

    class QuasiSingleton:
        @classmethod
        def make_key(cls, *args, **kwargs):
            ...
    
        def __new__(cls, *args, **kwargs):
            if 'cache' not in cls.__dict__:
                cls.cache = 
            return super().__new__(cls, *args, **kwargs)
    
        def __init__(self, *args, **kwargs):
            key = self.make_key(*args, **kwargs)
            if key in self.cache:  # Or more accurately type(self).instances
                data = self.cache[key]
            else:
                data = # Do lengthy computation
            # Initialize self with data object
    

    使用此选项,请记得致电super().__init__ 和(如果需要,请致电super().__new__)。

【讨论】:

示例帮助。选项 2 实际上是我正在寻找的。 (我的课程确实是不可变的)。虽然我之前已经忽略了它,但我现在也看到了选项 1 的优点。谢谢。 在选项 3 中,您为什么要费心定义 __new__?缓存类属性不能在方法定义之外初始化吗? @ibonyun。将新的 dict 代码放入 __new__ 可确保继承正常工作。我没有做正确的检查,所以现在已经解决了。

以上是关于如何创建一个不允许重复实例的类(在可能的情况下返回现有实例)?的主要内容,如果未能解决你的问题,请参考以下文章

允许默认构造函数接受一个实例并原封不动地返回它

JAVA如何在不复制新实例引用的情况下更改实例字段值[重复]

允许在不修改底层容器的情况下排序和删除项目的类(可能是代理)的命名

返回非实例化类类型的类型提示[重复]

是否可以在不调用数据库的情况下创建空IQueryable的实例?

单例模式