如何避免创建具有相同值的对象?

How to avoid creating objects with same values?

我需要创建一个 class,其实例不能具有相同的值。如果您使用已被使用的值创建实例,您将得到旧的相同实例。

我用特殊的class方法做到了:

class A():
    instances = []

    def __init__(self, val):
        self.val = val

    @classmethod
    def new(cls, val):
        """
        Return instance with same value or create new.
        """
        for ins in cls.instances:
            if ins.val == val:
                return ins
        new_ins = A(val)
        cls.instances.append(new_ins)
        return new_ins

a1 = A.new("x")
a2 = A.new("x")
a3 = A.new("y")

print a1  # <__main__.A instance at 0x05B7FD00> S\   /M\
print a2  # <__main__.A instance at 0x05B7FD00>   \A/   \E
print a3  # <__main__.A instance at 0x05B7FD28>

有没有不使用.new方法更优雅的方法?

如果你真的想让它更优雅,在__new__中实现重复检查,所以它会在你调用A(something)时执行。

就在__new__:

def __new__(cls, val=None):
    for i in cls.instances:
        if val == i.val:
            return i
    return object.__new__(cls)

你可以试试functools.lru_cache

例如:

from functools import lru_cache

class A:

    @lru_cache()
    def __new__(cls, arg):
        return super().__new__(cls)

    def __init__(self, arg):
        self.n = arg

示例用法:

>>> a1 = A('1')
>>> a2 = A('1')
>>> a1 is a2
True
>>> a1.n
'1'
>>> a2.n
'1'

或者您可以尝试构建自定义缓存 class,正如 Raymond Hettinger 在这条推文中所指出的:https://twitter.com/raymondh/status/977613745634471937.

这可以通过覆盖 __new__ method 来完成,后者负责创建 class 的新实例。每当你创建一个新实例时,你将它存储在一个字典中,如果字典包含一个匹配的实例,那么你 return 它而不是创建一个新实例:

class A:
    instances = {}

    def __new__(cls, val):
        try:
            return cls.instances[val]
        except KeyError:
            pass

        obj = super().__new__(cls)

        cls.instances[val] = obj

        return obj

    def __init__(self, val):
        self.val = val
a = A(1)
b = A(2)
c = A(1)

print(a is b)  # False
print(a is c)  # True

此解决方案的一个缺点是,无论实例是新创建的实例还是存储在字典中的实例,都会调用 __init__ 方法。如果您的构造函数有不良副作用,这可能会导致问题:

class A:
    ...

    def __init__(self, val):
        self.val = val
        self.foo = 'foo'


a = A(1)
a.foo = 'bar'
b = A(1)
print(a.foo)  # output: foo

注意在创建 bafoo 属性如何从 "bar" 更改为 "foo"。


另一种选择是使用 metaclass and override its __call__ 方法:

class MemoMeta(type):
    def __new__(mcs, name, bases, attrs):
        cls = super().__new__(mcs, name, bases, attrs)
        cls.instances = {}
        return cls

    def __call__(cls, val):
        try:
            return cls.instances[val]
        except KeyError:
            pass

        obj = super().__call__(val)

        cls.instances[val] = obj

        return obj


class A(metaclass=MemoMeta):
    def __init__(self, val):
        self.val = val
        self.foo = 'foo'

这绕过了在现有实例上调用 __init__ 的问题:

a = A(1)
a.foo = 'bar'
b = A(1)
print(a.foo)  # output: bar