3

Suppose I want to create instances of my class freely, but if I instantiate with the same argument, I want to get the same unique instance representing that argument. For example:

a = MyClass('Instance 1');
b = MyClass('Instance 2');
c = MyClass('Instance 1');

I would want a == c to be True, based on the unique identifier I passed in.

Note:

(1) I'm not talking about manipulating the equality operator-- I want a to really be the same instance as c.

(2) This is intended as library code, so uniqueness has to be enforced-- we can't just count on users doing the right thing (whatever that is).

Is there a canonical way of achieving this? I run into this pattern all the time, but I usually see solutions involving shadow classes, meant for only internal instantiation. I think I have a cleaner solution, but it does involve a get() method, and I'm wondering if I can do better.

  • This looks like an extension of *Singleton*. But in *Python* you can't enforce users to do / not do something, there are always ways to bypass any restriction. – CristiFati Jun 12 '18 at 15:30
  • This may help: http://python-3-patterns-idioms-test.readthedocs.io/en/latest/Singleton.html – DYZ Jun 12 '18 at 15:33
  • @17slim, from his question "I want a to really be the same instance as c." – user3483203 Jun 12 '18 at 15:35
  • Related, but not a duplicate: https://stackoverflow.com/questions/31875/is-there-a-simple-elegant-way-to-define-singletons and https://stackoverflow.com/questions/6760685/creating-a-singleton-in-python – Robᵩ Jun 12 '18 at 15:44
  • @Robᵩ Thank you-- the latter ref describes a metaclass implementation I didn't know about. That may be The Right Thing. –  Jun 12 '18 at 16:08
  • This is called a Registry of singletons (GoF's Design Patterns), or a Multiton: https://en.wikipedia.org/wiki/Multiton_pattern. – Aristide Nov 29 '19 at 10:24

2 Answers2

4

I'd use a metaclass. This solution avoids calling __init__() too many times:

class CachedInstance(type):
    _instances = {}
    def __call__(cls, *args):
        index = cls, args
        if index not in cls._instances:
            cls._instances[index] = super(CachedInstance, cls).__call__(*args)
        return cls._instances[index]

class MyClass(metaclass=CachedInstance):
    def __init__(self, name):
        self.name = name

a = MyClass('Instance 1');
b = MyClass('Instance 2');
c = MyClass('Instance 1');
assert a is c
assert a is not b

Reference and detailed explanation: https://stackoverflow.com/a/6798042/8747

Robᵩ
  • 163,533
  • 20
  • 239
  • 308
  • 1
    Normally, metaclasses scare me, but I like this one. It abstracts the actual caching logic away from the class so you don't have to think about it. My only (slight) reservation is that there is no quick way to do `MyClass.instances.clear()` (although that could be implemented, of course). – FHTMitchell Jun 12 '18 at 16:08
  • I didn't know about metaclasses. This looks like a winner. It preserves user's ability to use standard instantiation syntax. It preserves the target class's expression of properties and methods without requiring an internal class-- only the `metaclass=` qualifier. And the couple of tests I've run with an additional named arg (that will be the basis of determining uniqueness) all work as expected. Awesome, thank you. –  Jun 12 '18 at 16:13
  • @Robᵩ Actually your index composed of args permits me exactly the uniqueness among instances I require: a single instance for any unique set of arguments passed for instantiation. Perfect. –  Jun 12 '18 at 16:22
  • This looks good at first glance, but the implementation has a serious flaw: it doesn't support keyword arguments. That is, in my opinion, a deal breaker. You should consider adding some `inspect` module magic to make keyword arguments work. – Aran-Fey Jun 12 '18 at 16:23
  • @Aran-Fey - [Here](https://ideone.com/Q6SRZK) is such an implementation. – Robᵩ Jun 12 '18 at 16:52
  • 1
    With that, `MyClass('foo')` and `MyClass(name='foo')` are two different instances though. – Aran-Fey Jun 12 '18 at 16:53
2

This can be done (assuming that args are all hashable)

 class MyClass:

     instances = {}

     def __new__(cls, *args):
          if args in cls.instances:
               return cls.instances[args]
          self = super().__new__(cls)
          cls.instances[args] = self
          return self

a = MyClass('hello')
b = MyClass('hello')
c = MyClass('world')

a is b and a == b and a is not c and a != c  # True

is is the python operator that shows two objects are the same instance. == falls back to is on objects where it is not overidden.


As pointed out in the comments, this can be a bit troubling if you have an __init__ with side effects. Here's an implementation that avoids that:

class Coord:

     num_unique_instances = 0
     _instances = {}

     def __new__(cls, x, y):

         if (x, y) in cls._instances:
              return cls._instances[x, y]

         self = super().__new__(cls)

         # __init__ logic goes here  -- will only run once
         self.x = x
         self.y = y
         cls.num_unique_instances += 1
         # __init__ logic ends here

         cls._instances[x, y] = self
         return self

     # no __init__ method
FHTMitchell
  • 11,793
  • 2
  • 35
  • 47
  • The problem with this is that it calls `__init__` every time, even if the instance already existed. If you have an `__init__` method with side effects, this implementation won't cut it. – Aran-Fey Jun 12 '18 at 15:40
  • @Aran-Fey True, but you can put the `__init__` logic in `__new__` after the `self = super().__new__(cls)` line. But this gets tricky with subclassing. – FHTMitchell Jun 12 '18 at 15:42
  • @Aran-Fey Does the extraneous `__init__` just get thrown away? For my purposes, I might be OK with that overhead, as @FHTMitchell's form is syntactically exactly what I want. –  Jun 12 '18 at 15:46
  • @JohnPirie No the problem is that `__init__` is run every time, whether the class is new or not. So if you have a line of code that has side effects, it will happen every time someone gets an object using this method. It's also a bit wasteful to reassign `self.x = x` every time. -- see my edit – FHTMitchell Jun 12 '18 at 15:50