ekiim
ekiim

Reputation: 842

Changing `self` for another instance of same object?

I want to create a class, and all objects need to have a unique identifier key, and If I attempt to create a new instance of the object with a previously existent key, the instance should be the same as the one that already existing.

Similar to a singleton class, but in this case instead of one class, there are many but different.

My first approach was this

class Master:
    existent = {}
    def __init__(self, key, value=None):
        try:
            self = Master.existent[key]
            return 
        except KeyError:
            Master.existent[key] = self
        # Rest of the __init__ method

But when I compare two objects, something like this A = Master('A', 0) and B = Master('B', 0), the B doesn't share any attributes that It should have, and if the Master class has any _method (single underscore), It also doesn't appear.

Any Idea how could I do this?

I think this is similar to the Factory Methods Pattern, but I'm still having trouble to find the parallels, or how to implemented in an elegant form.

EDIT:

The class basically has two proprieties and that's it, but many things would Inherit and/or contain instances of this as type, the easy way I thought I could do it, was extracting the properties from the existing instance corresponding to said key, assigning them to the new instance and abuse from the fact that they will have same hash output and the the equal operator will behave according to hashes so I can use == and is operators with no problem.

This Idea solves my problem, but overall I think this could be a common or interesting enough scenario to tackle.

Upvotes: 0

Views: 711

Answers (3)

A Kruger
A Kruger

Reputation: 2419

You can use the __new__ method to handle this. You don't want to call __init__ unless you want to create a new object with a new key, and __new__ can be used to first check if the key is unique before calling __init__.

class Master(object):

    instances = {}
    def __new__(cls, key, value=None):
        if key in Master.instances:
            return Master.instances[key]
        else:
            instance = super(Master, cls).__new__(cls)
            Master.instances[key] = instance
            return instance

    def __init__(self, key, value=None):
        self.value = value

Then you can create the objects with

>>> A = Master('A',0)
>>> B = Master('B',0)
>>> C = Master('A',1)

Since A and C have the same key, they will point to the same object and will effectively be the same instance. Since C has the same key as A, it updates its value.

>>> print(A.value)
1

Any new changes to A will be seen in C, and vice versa.

>>> A.value = 5
>>> print(C.value)
5

But changes to A and C will not affect B, and changes to B will not affect A or C.

Edit:

If you want to copy values but not instances, you could just store the values in the Master.instances dictionary and check if there's already values for the key.

class Master(object):

    instances = {}
    def __init__(self, key, value=None):
        if key in Master.instances:
            self.value = Master.instances[key]
        else:
            self.value = value
            Master.instances[key] = value

>>> A = Master('A',0)
>>> C = Master('A',1)
>>> print(C.value)
0

Upvotes: 1

John Anderson
John Anderson

Reputation: 38962

Inspired by the answer from A Kruger, I have another solution building off the use of the __new__ method as suggested. The main difference in this answer is that there is no need to create an inner __Master class. The __new__ method is automatically called when Master() is invoked, and is expected to return an instance of the Master class. In my answer, the __new__ method returns a new instance, if needed, but returns an instance from the existent dictionary, if possible. Note that the user accesses the Master class as usual, i.e., they just call Master('A', 0). This is made possible by making the Master class extend object.

Here is the code:

class Master(object):
    existent = {}

    def __init__(self, key, value=None):
        self.key = key
        self.value = value
        if not key in Master.existent:
            Master.existent[key] = self

    def __new__(cls, *args, **kwargs):
        key = args[0]
        if key in Master.existent:
            return Master.existent[key]
        else:
            return super(Master, cls).__new__(cls)

    def __str__(self):
        return('id: ' + str(id(self)) + ', key=' + str(self.key) + ', value=' + str(self.value))

A = Master('A', 0)
print('A = ' + str(A))
B = Master('A', 1)
print('\nAfter B created:')
print('B = ' + str(B))
print('A = ' + str(A))
B.value = 99
print('\nAfter B modified:')
print('B = ' + str(B))
print('A = ' + str(A))
C = Master('C', 3)
print('\nC = ' + str(C))

And here is the output:

A = id: 140023450750200, key=A, value=0

After B created:
B = id: 140023450750200, key=A, value=1
A = id: 140023450750200, key=A, value=1

After B modified:
B = id: 140023450750200, key=A, value=99
A = id: 140023450750200, key=A, value=99

C = id: 140023450750256, key=C, value=3

Note that A and B have the same id (they are the same object). Also note that changes to A or B affect each other, since they are the same object.

Upvotes: 1

John Anderson
John Anderson

Reputation: 38962

I don't think you can do that using the __init__() method, because a new instance of the class has already been created when that method is called. You probably need to create a factory type method something like:

class Master:
    existent = {}
    init_OK = False

    def __init__(self, key, value=None):
        if not Master.init_OK:
            raise Exception('Direct call to Master() is not allowed')
        Master.init_OK = False
        self.key = key
        self.value = value
        Master.existent[key] = self

    @staticmethod
    def make(key, value=None):
        try:
            inst = Master.existent[key]
        except:
            Master.init_OK = True
            inst = Master(key, value=value)
        return inst

Upvotes: 1

Related Questions