关于python:如何创建不允许重复实例的类(如果可能,返回现有实例)?

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

我有数据,每个条目都需要是一个类的实例。我希望在我的数据中会遇到许多重复的条目。我基本上想以一组所有唯一的条目结束(即丢弃任何重复项)。然而,实例化整个批并在事实发生后将它们放入一个集合不是最佳的,因为…

  • 我有很多条目,
  • 重复条目的比例预计相当高,
  • 我的__init__()方法为每个唯一的条目做了很多昂贵的计算,所以我希望避免不必要地重复这些计算。
  • 我认识到这基本上是同一个问题,但…

  • 公认的答案实际上并不能解决问题。如果你让__new__()返回一个已有的实例,从技术上讲,它不会生成一个新的实例,但它仍然调用__init__(),它会恢复你已经完成的所有工作,这使得重写__new__()完全没有意义。(这很容易通过在__new__()__init__()中插入print语句来证明,这样您就可以看到它们何时运行。)

  • 另一个答案要求在需要新实例时调用类方法而不是调用类本身(例如:x = MyClass.make_new()而不是x = MyClass())。这是可行的,但它不是理想的imho,因为它不是人们通常认为的创建新实例的方式。

  • 是否可以重写__new__(),以便在不重新运行__init__()的情况下返回现有实体?如果这是不可能的,有没有其他方法来解决这个问题?


    假设您有一种识别重复实例的方法和此类实例的映射,那么您有几个可行的选项:

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    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

    我建议使用这个基类,特别是如果您的类在任何方面都是可变的。如果不明确说明一个实例可能是相同的,则不希望在另一个实例中显示对该实例的修改。执行EDOCX1[4]意味着每次都会得到不同的实例,或者至少您的实例是不可变的,您不在乎。

  • 在元类中重新定义__call__

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    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__相当于把__new____init__称为cls

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

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

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    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__)。