0
class TaskInput:
    def __init__(self):
        self.cfg = my_config #### Question: How do I do this only once?

class TaskA(TaskInput):
    def __init__(self):
        pass

class TaskB (TaskInput):
    def __init__(self):
        pass
  • There are many tasks like TaskA, TaskB etc, they all are inherited from TaskInput.
  • Tasks also depend on something, let's say, a configuration which I only want to set ONCE.
  • The code has multiple Tasks classes, like TaskA, TaskB etc. They all depend on this common configuration.

One natural way would be to make this configuration a class member of TaskInput, ie, TaskInput.cfg = my_config, something that's initialized in __init__() of TaskInput.

However, if it's a member of TaskInput, it'll get executed multiple times, every time a new object of type TaskX is created as all those Tasks are inherited from TaskInput.

What's the best practice and best way to accomplish this in Python?

AcK
  • 2,063
  • 2
  • 20
  • 27
shadyabhi
  • 16,675
  • 26
  • 80
  • 131
  • Not a direct duplicate but you can use a singleton or similar approach. See [this](https://stackoverflow.com/questions/6760685/creating-a-singleton-in-python) – Yevhen Kuzmovych Nov 21 '22 at 20:35
  • What does `TaskInput` provide aside from a wrapped configuration? In the code shown here, the configuration is just a global variable in disguise. – chepner Nov 21 '22 at 21:17
  • When you say "class member" that implies *class variable*, in Python's parlance, i.e. a "static member", so no, that would be initialized only one. You mean an *instance attribute*. In any case, a class attribute seems like a reasonable solution – juanpa.arrivillaga Nov 21 '22 at 22:19
  • But honestly, I see no reason why this shouldn't just be an instance attribute, one which is initialized in the subclasses with a call to `super().__init__()`. This would be fine. Why is this a problem? – juanpa.arrivillaga Nov 21 '22 at 22:20

2 Answers2

2

Make the configuration a class attribute by defining it on the class rather than in __init__.

class TaskInput:
    cfg = my_config

It is now accessible as self.cfg on any instance of TaskInput or its children.

If the configuration isn't available when the class is defined (e.g. it's read later from a file), assign it on the class when it is available:

TaskInput.cfg = my_config

You have two choices as to how to handle writing the class definition in this situation.

  1. Don't define cfg in the class definition at all, so you'll get a big juicy AttributeError if you try to access the configuration before it's available.

  2. Define a default configuration which gets overwritten when the real configuration is available.

Generally I favor approach #1, since it "fails fast" (i.e. detects logic errors in your code when they occur rather than hiding them until something goes screwy later), but there are situations where you might need a default configuration to get things up and running before you can read the "real" configuration. In that case the default configuration should be the bare minimum possible, just what you need to get going.

kindall
  • 178,883
  • 35
  • 278
  • 309
0

I will not try to guess your need so I will assume you mean exactly what you said below, namely that you want a single initialization of a class member, but done through the creation of an instance.

a class member of TaskInput, ie, TaskInput.cfg = my_config, something that's initialized in init() of TaskInput.

This can work, but not the way you did it. in your code you never created a class attribute, anything created with self is an instance attribute belonging to a single specific task instance so that:

from copy import deepcopy

class TaskInput:
    _cfg = None # prefix with '_' to indicate it should be considered private
    
    def __init__(self, my_config=None):
        _cfg = _cfg or my_config
    
    @property
    def cfg(self):
        """ If you want to privatize it a bit more,
        make yourself a getter that returns a deep copy."""
        return deepcopy(cfg)

Now, there basically is no such thing as true privatization in python and you will never be able to entirely prevent manipulation. In the example above, any child has direct read-write access to _cfg, so it would fall on us not to use it directly and pass by its accessors (__init__() and cfg()).

There's always a way to make things more difficult, like the following, using modules.

 Project
 ├─ __init__.py
 ├─ settings.py
 ├─ module1.py
 └─ module2.py

settings.py

cfg = None

module1.py

from copy import deepcopy
import settings

class A:
    def __init__(self, cfg_=None):
        settings.cfg = settings.cfg or cfg_

    @property
    def cfg(self):
        return deepcopy(settings.cfg)

module2.py

""" The following classes won't be able to
overwrite the config without importing
from settings.py.
"""

from module1 import A

class B(A):
    pass

class C(A):
    def __init__(self):
        super().__init__("foobar")

Giving these results:

b0 = B()
b0.cfg
# > None

b1 = B({"foo1": "bar1"})
b1.cfg
# > {'foo1': 'bar1'}

b2 = B({"foo1": "bar2", "foo3": "bar3"})
b2.cfg
# > {'foo1': 'bar1'}

try:
    b2.cfg = 1234
except Exception as e:
    print(type(e), e)
# > <class 'AttributeError'> can't set attribute

b2.cfg
# > {'foo1': 'bar1'}

c = C("asdf")
c.cfg
# > {'foo1': 'bar1'}

Which can be overkill of course and removes the actual ownership of the configuration from the class