Auto-generate unique ID for each class type

Viewed 592

I have an interesting problem. I would like to write a class, which when inherited provides to all children classes the following behavior:

  • sets its self.id attribute to a UUID value -- if this is the first time class got instantiated, a new UUID is generated -- the UUID value is reused for the same class, when it is instantiated many times

Now, the fun part: I want the mechanism above to work, regardless of the path used to instantiate this class. Let's assume the following:

from .Package.Class1 import Class1
from TopPackage.Package.Class1 import Class1

from .Package.Class2 import Class2
from TopPackage.Package.Class2 import Class2

In both situations, I would like Class1 to generate the same self.id value in both import styles. I would also like Class2 to generate a different self.id value from Class1, but the same between its own import styles.

So far, I wrote the following code for a class classes 1 and 2 would inherit from:

class ClassWithId(ABC):
    _EXISTING_OBJECT_IDS = dict()

    def __init__(self):
        if self in ClassWithId._EXISTING_OBJECT_IDS.keys():
            self.id = ClassWithId._EXISTING_OBJECT_IDS[self]
        else:
            self.id = uuid.uuid4()
            ClassWithId[self] = self.id

However, I have a few problems in here:

  • ClassWithId must inherit from class ABC because it is also an interface for classes 1 and 2
  • trying to put self as key in dict() results in TypeError: 'ABCMeta' object does not support item assignment
  • I am generally unsure, if this approach is going to be resistant to different import styles, because from Python's perspective class type .Package.Class1.Class1 and TopPackage.Package.Class1.Class1 are 2 different objects

Any ideas?

UPDATE: I have integrated Elrond's suggestion into my code, and but different import levels (package-wise) yield different UUID values for the same class:

<class 'StageTwo.Steps.SsTestHandler1.SsTestHandler1'>   3583c89c-5ba8-4b28-a909-31cc27628370
<class 'tests.TestStages.StageTwo.Steps.SsTestHandler1.SsTestHandler1'>   f4ead4a0-f5f7-4d95-8252-0de47104cb2f

<class 'StageTwo.Steps.SsTestHandler2.SsTestHandler2'>   8bd9a774-0110-4eee-a30c-a4263ad546cf
<class 'tests.TestStages.StageTwo.Steps.SsTestHandler2.SsTestHandler2'>   773d84c4-82a3-4684-92b5-51509e6db545

Maybe I forgot to mention, but my ClassWithId is inherited by ClassX and ClassY down the line, and it is these 2 classes, which are expected to be resistant to the situation I have shown above (being imported with different path, yet still retaining the same UUID).

2 Answers

You will need to use a metaclass for this:

import uuid
import abc

class BaseMeta(abc.ABCMeta):
    def __new__(mcs, name, bases, attrs):
        attrs['_class_uuid'] = uuid.uuid4()
        return super().__new__(mcs, name, bases, attrs)     

class Base(metaclass=BaseMeta):
    def __init__(self):
        print(self.__class__.__name__, self._class_uuid)

Now all classes that inherit from Base will be assigned a uuid via the _class_uuid property, once per subclass:

from package1.class1 import Class1
from package2.class2 import Class2

Class1() # 6e0852c8-61c9-4f8b-9817-eeeda4b49d56
Class1() # 6e0852c8-61c9-4f8b-9817-eeeda4b49d56

Class2() # 73012f1a-a984-4f76-96f1-ef5225a38fbe
Class2() # 73012f1a-a984-4f76-96f1-ef5225a38fbe

Using absolute/relative imports shouldn't make a difference in either case.

from Python's perspective class type .Package.Class1.Class1 and TopPackage.Package.Class1.Class1 are 2 different objects

If I understand what you're saying here, I don't think this statement is true. Hopefully, my example below will clarify.

There might be other approaches, such as using the class name as your dictionary key, but perhaps a more extendable approach is with a metaclass.

I placed everything in one file, for the sake of simplicity, but the approach remains the same for a module with multiple levels:

TopPackage.py:

import uuid

class ABC:
    pass

def id_assigner(class_name, class_parents, class_attrs):
    class_attrs['id'] = str(uuid.uuid4())
    return type(class_name, class_parents, class_attrs)

class Class1(ABC, metaclass=id_assigner):
    pass
    
class Class2(ABC, metaclass=id_assigner):
    pass

def foo():
    c2 = Class2()

    print('c2.id from foo: ', c2.id)

And a script to test it:

import TopPackage

c1_a = TopPackage.Class1()
c1_b = TopPackage.Class1()

print(c1_a.id)
print(c1_b.id)

c2_a = TopPackage.Class2()
c2_b = TopPackage.Class2()

print(c2_a.id)
print(c2_b.id)

TopPackage.foo()

The call to TopPackage.foo(), I hope, shows that using Class2 from different location will result in the same class definition, and hence the same id. When I run this test script an example output I get is:

c69b17e0-9ff0-4276-bcce-6ac4f5e5a2e5
c69b17e0-9ff0-4276-bcce-6ac4f5e5a2e5
86fbe02e-d411-4ba1-b292-d2b1ec2100bd
86fbe02e-d411-4ba1-b292-d2b1ec2100bd
c2.id from foo:  86fbe02e-d411-4ba1-b292-d2b1ec2100bd
Related