3

I am trying to create a class that doesn't re-create an object with the same input parameters. When I try to instantiate a class with the same parameters that were used to create an already-existing object, I just want my new class to return a pointer to the already-created (expensively-created) object. This is what I have tried so far:

class myobject0(object):
# At first, I didn't realize that even already-instantiated
# objects had their __init__ called again
instances = {}
def __new__(cls,x):
    if x not in cls.instances.keys():
        cls.instances[x] = object.__new__(cls,x)
    return cls.instances[x]
def __init__(self,x):
    print 'doing something expensive'

class myobject1(object):
    # I tried to override the existing object's __init__
    # but it didnt work.
    instances = {}
    def __new__(cls,x):
        if x not in cls.instances.keys():
            cls.instances[x] = object.__new__(cls,x)
        else:
            cls.instances[x].__init__ = lambda x: None
        return cls.instances[x]
    def __init__(self,x):
        print 'doing something expensive'

class myobject2(object):
    # does what I want but is ugly
    instances = {}
    def __new__(cls,x):
        if x not in cls.instances.keys():
            cls.instances[x] = object.__new__(cls,x)
            cls.instances[x]._is_new = 1
        else:
            cls.instances[x]._is_new = 0
        return cls.instances[x]
    def __init__(self,x):
        if self._is_new:
            print 'doing something expensive'

This is my first venture into overriding __new__ and I'm convinced I'm not going about it the right way. Set me straight, please.

Jörg W Mittag
  • 363,080
  • 75
  • 446
  • 653
Paul
  • 42,322
  • 15
  • 106
  • 123

3 Answers3

15

Here's a class decorator to make a class a multiton:

def multiton(cls):
   instances = {}
   def getinstance(id):
      if id not in instances:
         instances[id] = cls(id)
      return instances[id]  
   return getinstance

(This is a slight variant of the singleton decorator from PEP 318.)

Then, to make your class a multiton, use the decorator:

@multiton
class MyObject( object ):
   def __init__( self, arg):
      self.id = arg
      # other expensive stuff

Now, if you instantiate MyObject with the same id, you get the same instance:

a = MyObject(1)
b = MyObject(2)
c = MyObject(2)

a is b  # False
b is c  # True
Jerry Neumann
  • 415
  • 1
  • 4
  • 11
  • This is by far a cleaner and just as efficient and simple solution as suggested by S.Lott. – Erik Kaplun Jun 24 '12 at 18:27
  • @ErikAllik, you write, "Using `__new__` would indeed be the/a pythonic way of doing it." Do you prefer @Jerry's solution to one that uses `__new__`? – kuzzooroo Jan 04 '14 at 01:37
  • @kuzzooroo: I would say yes—Jerry's solution is less magic and more straighforward, and Pythonic at the same time. – Erik Kaplun Jan 04 '14 at 18:48
  • 1
    Minor nitpick for future readers' information: Use a name like `key` or `name` for `getinstance`'s parameter, rather than `id`, to avoid shadowing the Python builtin. –  Jun 03 '18 at 00:47
8

First, use Upper Case Class Names in Python.

Second, use a Factory design pattern to solve this problem.

class MyObject( object ):
    def __init__( self, args ):
        pass # Something Expensive

class MyObjectFactory( object ):
    def __init__( self ):
        self.pool = {}
    def makeMyObject( self, args ):
        if args not in self.pool:
            self.pool[args] = MyObject( args )
        return self.pool[args]

This is much simpler than fooling around with new and having class level pools of objects.

S.Lott
  • 384,516
  • 81
  • 508
  • 779
  • This is exactly the best way to do it. Perfectly suggested. – mpeterson Mar 23 '09 at 03:31
  • 2
    Using `__new__` would indeed be the/a pythonic way of doing it as opposed to introducing another class—your solution is idiomatic Java. Also, don't insert spaces inside parentheses and don't use camelCase method names in Python. – Erik Kaplun Jun 24 '12 at 18:21
1

Here is my implementation of Jerry's way, using an array as the pool

def pooled(cls):
    """
    decorator to add to a class, so that when you call YourClass() it actually returns an object from the pool
    """

    pool = []

    def get_instance(*args, **kwargs):
        try:
            instance = pool.pop()
        except IndexError:
            instance = cls(*args, **kwargs)
        returned_instance = yield instance
        pool.append(returned_instance)
        print(len(pool))
        yield

    return get_instance


@pooled
class MyClass():
    def __init__(self, num):
      self.num = num


for i in range(10):
    m_gen =MyClass(i)
    n_gen = MyClass(i + 5)
    m = next(m_gen)
    n = next(n_gen)
    print(f'm num: {m.num}')
    print(f'n num: {n.num}')
    m_gen.send(m)
    n_gen.send(n)

and then another way using metaclasses so you can inherit the functionality. This one uses weakreaf valuedicts as the pool, so the objects get garbage collected better

import weakref
  
class PooledMeta(type):
  def __init__(self, *args, **kwargs):
    super().__init__(*args, **kwargs)
    self._pool = weakref.WeakValueDictionary()

  def __call__(self, *args):
    if args in self._pool:
      print('got cached')
      return self._pool[args]
    else:
      # print(self._pool.valuerefs())
      instance = super().__call__(*args)
      self._pool[args] = instance
      return instance

class MyPooled(metaclass=PooledMeta):
  def __init__(self, num):
    print(f'crating: {num}')
    self.num = num

class MyPooledChild(MyPooled):
  def __init__(self, num):
    print(f'crating child: {num}')
    self.num = num

p = []
for i in range(10):
  m = MyPooled(i)
  n = MyPooledChild(i)
  p.extend([m,n])