0
\$\begingroup\$

In my Python projects, I often define parameters in a TOML, YAML, or JSON file. Those parameters are loaded into a dictionary which is utilized by various functions in the project. See below for some examples. I'm curious on how others would approach this and if there are better ways to work with functions and parameter files.

Parameters file

Parameters are defined in a TOML file named params.toml.

[feedstock]
d = 0.8
phi = [ 0.65, 0.8, 0.95 ]
k = 1.4
cp = 1800
temp = 60
ei = 1.2
eo = 1.8
rho = 540

[reactor]
d = 5.4
h = 8.02
temp = 500
p = 101325

The parameters are loaded into a dictionary named params.

import toml

pfile = 'params.toml'

with open(pfile, 'r') as f:
    params = toml.load(f)

Example 1

This example explicitly defines each input variable to the function. I like this example because it is obvious on what the inputs are to the function. Values from the parameters dictionary are assigned to variables which are used as inputs to the function.

def calc_feedx(d, rho, temp):
    a = (1 / 4) * 3.14 * (d**2)
    x = a * rho * temp
    return x

d = params['feedstock']['d']
rho = params['feedstock']['rho']
temp = params['feedstock']['temp']

x = calc_feedx(d, rho, temp)

Example 2

This example only has one input variable to the function which is a dictionary that contains all the parameters utilized by the function. I don't like this approach because it's not obvious what the input parameters are for the function. This example provides the entire dictionary to the function which accesses the parameters from within the function. Not all the parameters defined in the dictionary are used by the function.

def calc_feedx(params):
    d = params['feedstock']['d']
    rho = params['feedstock']['rho']
    temp = params['feedstock']['temp']

    a = (1 / 4) * 3.14 * (d**2)
    x = a * rho * temp
    return x

x = calc_feedx(params)
\$\endgroup\$
6
  • \$\begingroup\$ Use example 1, but please show all of your code. \$\endgroup\$
    – Reinderien
    Sep 20, 2021 at 3:51
  • \$\begingroup\$ @Reinderien This is all of the code. It's just an example I made up for my question. \$\endgroup\$
    – wigging
    Sep 20, 2021 at 4:19
  • \$\begingroup\$ There is a third option--use calc_feedx(**params). It shortens code a lot, but violates PEP 20's "Explicit is better than implicit.". I think all three are reasonable options. \$\endgroup\$ Sep 21, 2021 at 1:58
  • \$\begingroup\$ @ZacharyVance Your suggestion does not work because inputs to calc_feedx(d, rho, temp) are only d, rho, and temp. Using **params causes an error because the dictionary contains more parameters than what the function uses. \$\endgroup\$
    – wigging
    Sep 21, 2021 at 3:55
  • \$\begingroup\$ That's a good point I didn't notice. You could add a **kwargs to calc_feedx which is silently discarded, but that's pretty ugly. \$\endgroup\$ Sep 21, 2021 at 4:00

1 Answer 1

1
\$\begingroup\$

What is missing is context. Example one is fine, but what if it contained 10 or 15 parameters? That means that you probably have some objects hiding in there.

The problem with the second example is that you pass the whole params object in, but only need feedstock. calc_feedx(feedstock) would be much more appropriate, which makes it basically equivalent to the first example.

Which brings me to this point - params object should not live everywhere. In fact it should stay as far away from your main logic as possible. What if you decide to host configuration on a server, are you going to rewrite the logic? Also, what if you change the names you use in the core of the app or in configuration? (These are just the examples off the top of my head). What I'm basically saying is that you should have configuration decoupled from the logic to avoid a potential mess.

So don't think how you will make the code work with configuration, but how will you make configuration work with code if that makes sense. The way you can go about reading configuration is basically endless and the structure of the configuration could also be independent of your app.

Edit:

Here is the example:

def read_toml_config(path):
  def read(path):
    with open(self.file, 'r') as f:
      return toml.load(f)

  def map(raw_config):
    return { "foo": raw_config["bar"] }

  raw_config = read(path)
  return map(raw_config)

# You decide how config object should look for your app
# because the properties are determined by the read_toml_config
# function and not by the path.toml file
config = read_toml_config('path.toml')

# calc_feedx does not know that the config exists
# all it knows is that foo exists, which it needs
calc_feedx(config["foo"])

You could generify read_toml_config to any other configuration -> you read it in some way and then you map it to your application's needs. And you don't pass the whole configuration around, but just the objects the functions need. And at the same time, you might not read in the whole configuration from path.toml, but just the values you need -> the code becomes the source of truth.

\$\endgroup\$
3
  • \$\begingroup\$ I think I understand what you're saying. Can you provide a code example of how you would decouple the parameters file (configuration) from the logic? \$\endgroup\$
    – wigging
    Sep 20, 2021 at 18:30
  • \$\begingroup\$ Mapping the dictionary to another dictionary seems redundant to me. For my work, the parameter names defined in the TOML file are descriptive so there is no need to rename them elsewhere in the project. The parameter names in the code are typically consistent with their names in the parameters file. \$\endgroup\$
    – wigging
    Sep 21, 2021 at 4:04
  • \$\begingroup\$ You don't always need to map, but it is useful to have the config object defined by your code -> this is the responsibility of read_toml_config. \$\endgroup\$
    – Blaž Mrak
    Sep 21, 2021 at 8:36

Not the answer you're looking for? Browse other questions tagged or ask your own question.