3

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).

Greem666
  • 919
  • 13
  • 24
  • The most important problem with your code is: Don't use `self` as a dictionary key. Try using `self.__class__.__name__` and see if it already solves your problem. – Wups Oct 01 '20 at 07:47
  • That would be one fix, however this will fail IF class names are not unique. I cannot guarantee that they will always be unique, hence I needed a UUID solution instead. I want to avoid having to overwrite UUID class attribute in each class, which inherits from ClassWithId, because it is not elegant, and welcomes issues where person creating that child class will forget to do so. – Greem666 Oct 01 '20 at 07:53
  • Still, `self` is the instance itself, it will always be different for every instance of the class. – Wups Oct 01 '20 at 08:24

2 Answers2

1

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.

Lord Elrond
  • 13,430
  • 7
  • 40
  • 80
  • This solution does not seem to work in a way you explained. When I have the same class instantiated in a few different modules with different import styles, they all seem to be getting a new UUID, which fails later comparison of these classes (all should be recognized as the same). – Greem666 Oct 01 '20 at 06:36
  • @Greem666 are the modules being executed in the same process, or in separate processes? The uuid will be re-generated each time the program is executed – Lord Elrond Oct 01 '20 at 17:12
  • they are re-generated depending on import path during one unit tests run via unittest module. See my update to main post above for details. – Greem666 Oct 01 '20 at 23:21
  • @Greem666 can you update your code to include example tests for your classes, how they are imported, and how the tests are executed? – Lord Elrond Oct 02 '20 at 19:57
0

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
brady
  • 2,227
  • 16
  • 18
  • Hi @brady, thank you for your reply. Your solution generates the same uuid value for me across all classes with metaclass set to id_assigner function. – Greem666 Oct 01 '20 at 06:51