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

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

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

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

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

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

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

【问题讨论】:

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

标签: python class duplicates instance


【解决方案1】:

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

  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) 意味着您每次都获得不同的实例,或者至少您的实例是不可变的并且您不在乎。

  2. 在您的元类中重新定义 __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__ 并没有多大意义。

  1. 第三种混合选项是可能的。使用此选项,您将创建一个新实例,但从现有实例复制__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__ 可确保继承正常工作。我没有做正确的检查,所以现在已经解决了。
猜你喜欢
  • 2020-07-15
  • 1970-01-01
  • 1970-01-01
  • 2018-01-21
  • 2015-07-19
  • 2015-04-02
  • 2012-12-19
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多