如何创建一个不允许重复实例的类(在可能的情况下返回现有实例)?
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如何在不复制新实例引用的情况下更改实例字段值[重复]
允许在不修改底层容器的情况下排序和删除项目的类(可能是代理)的命名