Initializing class variables only once

12.1k views Asked by At
class db: 

      global_ids = {}
      global_mappings = {}
      def __init__:

          db_client     = sth sth #clinet to mongo db
          db_connection = sth sth #connection to mongo db 
          db_coll        = sth sth

I need some class variables (global ids and global mappings) that can be initialized only once for class db and all other instances of db class can use it.

Value of class variables has to be calculated using by some function which looks up into database. How can I structure my class?

4

There are 4 answers

1
shx2 On

You can think of this as having a memoized class property.

While classproperty is not a python builtin, it is easy to implement:

class classproperty(object):
    """ @classmethod+@property """
    def __init__(self, f):
        self.f = classmethod(f)
    def __get__(self, *a):
        return self.f.__get__(*a)()

This can be used like:

class A:
    @classproperty
    def client1(cls):
        print("creating a client1 for %s..." % cls)
        return 0

print(A.client1)
print(A.client1)
print(A().client1)

Output:
creating a client1 for <class '__main__.A'>...
0
creating a client1 for <class '__main__.A'>...
0
creating a client1 for <class '__main__.A'>...
0

What's left now is memoizing it, so the decorated function only gets called once, and future calls return the value computed the first time. This can be done by "injecting" the value into A.__dict__, which causes future calls to access it directly, instead of the classproperty:

class memoized_classproperty(object):
    """ @classmethod+@property """
    def __init__(self, f):
        self.f = classmethod(f)
    def __get__(self, instance, owner):
        # get the value:
        value = self.f.__get__(instance, owner)()
        # inject the value into class's __dict__ before returning:
        attr = self.f.__func__.__name__
        setattr(owner, attr, value)
        return value

class A:
    @memoized_classproperty
    def client2(cls):
        print("creating a client2 for %s..." % cls)
        return 0

print(A.client2)
print(A.client2)
print(A().client2)

Output:
creating a client2 for <class '__main__.A'>...
0
0
0
0
martineau On

I would put all the global class variables in a separate class that is smart about initializing itself, and then create an instance of it in the database class's __init__() method, as illustrated in the code below:

class DBGlobals(object):
    initialized = False
    ids = {}
    mappings = {}

    def __init__(self):
        if not self.initialized:
            print('initializing globals')
            self.ids = {'ans': 41}          # initialize as needed
            self.mappings = {'v1': 'v2'}    # initialize as needed
            self.initialized = True

class DB(object):
    dbglobals = DBGlobals()
    def __init__(self):
        db_client     = sth sth #clinet to mongo db
        db_connection = sth sth #connection to mongo db
        db_coll       = sth sth

You can reference the global data in class methods with self.dbglobals.ids and self.dbglobals.mappings respectively. It would also be possible to pass arguments to the constructor of the globals class to customize it's creation if needed.

2
Martijn Pieters On

You can always test if those values have been set first; if you are not using threads that is as simple as:

class db: 
      global_ids = {}
      global_mappings = {}

      def __init__(self):
          self.db_client     = sth sth #clinet to mongo db
          self.db_connection = sth sth #connection to mongo db 
          self.db_coll        = sth sth

          if not db.global_ids:
              # set key-value pairs on db.global_ids

          if not db.global_mappings:
              # set key-value pairs on db.global_mappings

That way you defer setting those class attributes until the first time you create an instance of the class.

You can also opt to set those same values when defining the class, of course.

0
JL Peyret On

An alternative based on boolean test short-circuits

(depending on what you put in setUpClass, you could easily subclass behavior)

class db1:

    global_ids = {}
    global_mappings = {}

    @classmethod
    def setUpClass(cls):
        print("...running setUpClass on %s..." % (cls.__name__))
        cls.global_ids = {"port": 8000}
        cls.global_mappings: {"global": "mappings"}

    def __init__(self, message):
        print("%s.__init__(%s)" % (self.__class__.__name__, message))

        # setUpClass only gets called if global_ids is Falsy
        self.global_ids or self.setUpClass()
        print("  port:%s" % (self.global_ids["port"]))

class db2(db1):
    # global_ids are recomputed for db2
    global_ids = {}

class db3(db1):
    # but db3 and db1 share the same settings
    pass

db1("first")
db1("second - reuse first")
db2("third  - recompute for this class")
db3("fourth - reuse first")

output:

db1.__init__(first)
...running setUpClass on db1...
  port:8000
db1.__init__(second - reuse first)
  port:8000
db2.__init__(third  - recompute for this class)
...running setUpClass on db2...
  port:8000
db3.__init__(fourth - reuse first)
  port:8000