如何制作一个 class 不允许重复实例(尽可能返回现有实例)?

How to make a class which disallows duplicate instances (returning an existing instance where possible)?

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

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

我知道这基本上是同一个问题 但是...

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

  2. 另一个答案需要调用 class 方法,而不是在需要新实例时调用 class 本身(例如:x = MyClass.make_new() 而不是 x = MyClass()).这行得通,但恕我直言,这并不理想,因为这不是人们认为创建新实例的正常方式。

是否可以覆盖 __new__() 以便它将 return 现有实体 没有 运行 重新 __init__() ?如果这不可能,是否有其他方法可以解决这个问题?

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

  1. 使用 classmethod 为您获取实例。 class 方法与您的 metaclass 中的 __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
    

    我建议使用这个基数 class,尤其是如果您的 class 是可变的。您不希望一个实例的修改出现在另一个实例中,而没有明确说明这些实例可能是相同的。做 cls(*args, **kwargs) 意味着你每次都得到一个不同的实例,或者至少你的实例是不可变的,你不在乎。

  2. 在你的 metaclass 中重新定义 __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 可以直观地告知用户他们正在获得一个重复的实例。使用对 class 对象的正常调用意味着该实例将始终是新的,因此应该只对不可变的 classes.

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

  1. 第三个混合选项是可能的。使用此选项,您将创建一个新实例,但从现有实例复制 [​​=21=] 计算的昂贵部分,而不是重新做一遍。如果通过 metaclass 实现,此版本不会造成任何问题,因为所有实例实际上都是独立的:

    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__ 如果需要)。