1

I have a Python class in a base_params.py module within an existing codebase, which looks like this:

import datetime

class BaseParams:
    TIMESTAMP = datetime.datetime.now()
    PATH1 = f'/foo1/bar/{TIMESTAMP}/baz'
    PATH2 = f'/foo2/bar/{TIMESTAMP}/baz'

Callers utilize it this way:

from base_params import BaseParams as params

print(params.PATH1)

Now, I want to replace the TIMESTAMP value with one that is dynamically specified at runtime (through e.g. CLI arguments).

Is there a way to do this in Python without requiring my callers to refactor their code in a dramatic way? This is currently confounding me because the contents of the class BaseParams get executed at 'compile' time, so there is no opportunity there to pass in a dynamic value as it's currently structured. And in some of my existing code, this object is being treated as "fully ready" at 'compile' time, for example, its values are used as function argument defaults:

def some_function(value1, value2=params.PATH1):
    ...

I am wondering if there is some way to work with Python modules and/or abuse Python's __special_methods__ to get this existing code pattern working more or less as-is, without a deeper refactoring of some kind.

My current expectation is "this is not really possible" because of that last example, where the default value is being specified in the function signature. But I thought I should check with the Python wizards to see if there may be a suitably Pythonic way around this.

joelpt
  • 4,675
  • 2
  • 29
  • 28
  • 1
    Does this answer your question? [Creating a singleton in Python](https://stackoverflow.com/questions/6760685/creating-a-singleton-in-python) – Samwise Jun 25 '21 at 23:31
  • @Samwise I don't see how that is a duplicate – DeepSpace Jun 25 '21 at 23:32
  • `params` is essentially a singleton object. The particular way OP has chosen to implement this singleton (i.e. as a class with class attributes) has some drawbacks, but the answers in the linked duplicate have some really good alternatives that will allow them to very easily solve this problem. – Samwise Jun 25 '21 at 23:36
  • @Samwise technically correct (and also better practice in general) but I believe every solution suggested there will require to change the using code, which OP prefers not to. – DeepSpace Jun 25 '21 at 23:41

2 Answers2

1

Yes, you just need to make sure that the command line argument is parsed before the class is defined and before any function that uses the class's attribute as a default argument is defined (but that should already be the case).

(using sys.argv for sake of simplicity. It is better to use an actual argument parser such as argparse)

import datetime
import sys

class BaseParams:
    try:
        TIMESTAMP = sys.argv[1]
    except IndexError:
        TIMESTAMP = datetime.datetime.now()
    PATH1 = f'/foo1/bar/{TIMESTAMP}/baz'
    PATH2 = f'/foo2/bar/{TIMESTAMP}/baz'


print(BaseParams.TIMESTAMP)

$ python main.py dummy-argument-from-cli

outputs

dummy-argument-from-cli

while

$ python main.py

outputs

2021-06-26 02:32:12.882601
DeepSpace
  • 78,697
  • 11
  • 109
  • 154
  • Thank you, I ended up using this approach as it is obvious and doesn't require any 'magic'. – joelpt Jul 08 '21 at 20:39
1

You can still totally replace the value of a class attribute after the class has been defined:

 BaseParams.TIMESTAMP = <whatever>

There are definitely some more "magic" things you can do though, such as a class factory of some kind. Since Python 3.7 you can also take advantage of module __getattr__ to create a kind of factory for the BaseParams class (PEP 562)

In base_params.py you might rename BaseParams to _BaseParams or BaseParamsBase or something like that :)

Then at the module level define:

def __getattr__(attr):
    if attr == 'BaseParams':
        params = ... # whatever code you need to determine class attributes for BaseParams
        return type('BaseParams', (_BaseParams,), params)

    raise AttributeError(attr)
Iguananaut
  • 21,810
  • 5
  • 50
  • 63