2

I need to create a class whose instances can't have same values. If you create instance with value that have already been used you'll get old same instance.

I did it using special class method:

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>

Is there a way to do it more elegant, without using .new method?

Achal
  • 11,821
  • 2
  • 15
  • 37
dodd0ro
  • 467
  • 5
  • 12
  • `raise ValueError("Can't have objects with same values")` – iBug Mar 25 '18 at 16:16
  • But, a1 and a2 are essentially two references to **the same object**, which is unavoidable. Your current code well prevents **different objects** with same value from being created, but you can't stop an object from having two references. – iBug Mar 25 '18 at 16:18
  • 3
    Maybe you can do it with `__new__` or `__init__`. – iBug Mar 25 '18 at 16:21
  • You could do the new method inside __init__ – Liron Lavi Mar 25 '18 at 16:22
  • @iBug, code that "_prevents different objects with same value from being created_" is what i want. Having several references to the same object is ok. – dodd0ro Mar 25 '18 at 16:23
  • What if you change one of the cached instances? It would be trivial to assign e.g. `a2.val = 'y'`; then what? – jonrsharpe Mar 25 '18 at 16:26

3 Answers3

3

You could try functools.lru_cache.

For example:

from functools import lru_cache

class A:

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

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

Sample usage:

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

Alternatively you could try building a custom caching class, as pointed out by Raymond Hettinger in this tweet: https://twitter.com/raymondh/status/977613745634471937.

bla
  • 1,840
  • 1
  • 13
  • 17
2

If you really want to make it more elegant, implement the duplicate check in __new__, so it will be performed when you call A(something).

Just do it in __new__:

def __new__(cls, val=None):
    for i in cls.instances:
        if val == i.val:
            return i
    return object.__new__(cls)
iBug
  • 35,554
  • 7
  • 89
  • 134
2

This can be done by overriding the __new__ method, which is responsible for creating new instances of a class. Whenever you create a new instance you store it in a dict, and if the dict contains a matching instance then you return it instead of creating a new one:

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

One downside of this solution is that the __init__ method will be called regardless of whether the instance is a newly created one or one that's been stored in the dict. This can cause problems if your constructor has undesired side effects:

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

Notice how a's foo attribute changed from "bar" to "foo" when b was created.


Another option is to use a metaclass and override its __call__ method:

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'

This bypasses the problem with __init__ being called on existing instances:

a = A(1)
a.foo = 'bar'
b = A(1)
print(a.foo)  # output: bar
Aran-Fey
  • 39,665
  • 11
  • 104
  • 149
  • Thanks, it worked for me. You mentioned important thing that i missed: `Notice how a's foo attribute changed`. – dodd0ro Mar 25 '18 at 18:21
  • One thing i changed is onelining `__call__` method: `def __call__(cls, arg): return cls.instances.setdefault(arg, super(MemoMeta, cls).__call__(arg))` – dodd0ro Mar 25 '18 at 18:23
  • 1
    @dodd0ro That's not a great idea. First of all, it's harder to read if it's a single line, and secondly that will create a new instance _every time_, and then throw it away. (It creates an instance, that instance is passed to `setdefault`, and if the key already existed in the dict then the instance is discarded.) – Aran-Fey Mar 25 '18 at 18:25