Home] [About] [Posts] [Resources]
dir: Home /
Posts /
Python Hijinks - Permuting Params
published-date: 01 Jan 2025 00:15 +0700
categories: [quickie] [python-hijinks]
tags: [python]
Before we begin I must say this yet another “might not be best practices and not within standard of writing in python nor extendable to another language” ordeal. But to some extent with python’s “everything is object” mentality, it’s doable.
When training or testing a model, or trying out different parameters on a program you made, you might need to input sets of different parameters in order to map different behavior on a given value(s). One thing though, passing arguments and/or using config file, iterating different config and running with a script, sometimes is not feasible when working on notebooks (sometimes). Not mentioning the syntax to get these values, whether using a json
or configparser
, nuh-uh. Even wrapping these as dataclass
is just an added tech-debt when you need to add another parameter.
I prefer storing these params in a nested classes as a way to differentiate sections like as follows.
1class config:
2
3 class default:
4 path = './foo'
5 chunk_size = 2**16
6 seed = 1337
7
8 class rl:
9 batch_size = 256
10 gamma = 0.99
11 eps_start = 0.9
12 eps_end = 0.05
13 eps_decay = None # None value will fallback to `request_size_per_step * 2
14 num_episode = 1000
15 tau = 0.005
16 lr = 1e-4
17 ...
18 # and so on and so forth
The reason? With this approach I can make quick changes, no weird prefixes to differentiate same variable names across contexts, adding inline comments, and with autocompletion a blazing fast referencing variables experience just like a dataclass would. Problem arises when I need to input different pairs to compare against.
Thus I implemented a way to iterate over cartesian product of values straight from the config namespace, with little to no effort on changing the stuff I already wrote.
1import typing as t # entirely optional, and I might be wrong on these type hinting business but I already wrote it so why not
2import itertools
3
4class PermutableConfig:
5 _sect_prfx = 'section_'
6 _attr_prfx = '_permute_'
7
8 @classmethod
9 def _copy(cls) -> 'PermutableConfig':
10 cp = type(cls.__name__ + 'Copy', cls.__bases__, dict(cls.__dict__))
11 for section in cls._sections():
12 section_cls = getattr(cls, section)
13 setattr(cp, section, type(section_cls.__name__ + 'Copy', section_cls.__bases__, dict(section_cls.__dict__)))
14 return cp
15
16 @classmethod
17 def _sections(cls) -> t.Generator[str, None, None]:
18 yield from filter(lambda s: s.startswith(cls._sect_prfx), dir(cls))
19
20 @classmethod
21 def _entries(cls) -> t.Tuple[str, str, str]:
22 for section in cls._sections():
23 section_cls = getattr(cls, section)
24 for attr in filter(lambda s: s.startswith(cls._attr_prfx), dir(section_cls)):
25 values = getattr(section_cls, attr)
26 if not isinstance(values, (tuple, list, set)): continue
27 attr_trunc = attr[len(cls._attr_prfx):]
28 values = [getattr(section_cls, attr_trunc), *values]
29 yield section, attr_trunc, values
30
31 @classmethod
32 def permute_count(cls):
33 r = 1
34 for _, _, values in cls._entries():
35 r *= len(values)
36 return r
37
38 @classmethod
39 def permute(cls) -> t.Generator['PermutableConfig', None, None]:
40 sections, attrs, values = zip(*tuple(cls._entries()))
41 for vals in itertools.product(*values):
42 copy = cls._copy()
43 for n, value in enumerate(vals):
44 copy_section = sections[n]
45 copy_section_cls = getattr(copy, copy_section)
46 setattr(copy_section_cls, attrs[n], value)
47 yield copy
48
49 @classmethod
50 def to_dict(cls):
51 res = dict()
52 reserved = dir(object)
53 for section in cls._sections():
54 section_cls = getattr(cls, section)
55 if not section in res:
56 res[section] = dict()
57 for attr in filter(lambda s: not s.startswith('_') and not s in reserved, dir(section_cls)):
58 res[section][attr] = getattr(section_cls, attr)
59 return res
Now the new config namespace only need to inherit the class above.
For instance I want to observe over different learning rate, episode, batch size, and chunk size. Now config should looks like this.
1class config(PermutableConfig):
2
3 class section_default:
4 path = './foo'
5 chunk_size = 2**16
6 _permute_chunk_size = [2**14, 2**17]
7 seed = 1337
8
9 class section_rl:
10 batch_size = 256
11 _permute_batch_size = [64, 128]
12 gamma = 0.99
13 eps_start = 0.9
14 eps_end = 0.05
15 eps_decay = None # None value will fallback to `request_size_per_step * 2
16 num_episode = 1000
17 _permute_num_episode = [100, 5000, 10_000]
18 tau = 0.005
19 lr = 1e-4
20 _permute_lr = [1e-5, 1.5e-5]
21 ...
22 # on and on
Notice the class and the variable names prefixes. The section
prefix is used to filter out other namespace that will be copied over the generator (note that anything that doesn’t have the prefix will not be carried over), and _permute
prefix indicate that the variable after that prefix should be iterated over. Overall I only need to direct initial sections with the prefix added.
Right then, how shall this be used?
Tbh I don’t like to use global variable (the config
class itself already is), avoiding cluttered var names all over and accidentally assign values on them. I always pass these namespaces as args on any function or object method that needs them. Which in a way I can do as follows.
1
2def function_that_receives_config_namespace_as_arg(cfg: config):
3 # do stuff
4 ...
5
6class OrObjectThatNeedConfigOnInit:
7 def __init__(self, cfg: config):
8 # you get the idea
9 ...
10
11
12# if I ever need to run initial configuration, then
13function_that_receives_config_namespace_as_arg(config)
14thing = OrObjectThatNeedConfigOnInit(config)
15
16# or if I need to iterate over other values
17for new_config in config.permute():
18 function_that_receives_config_namespace_as_arg(new_config)
19 thing = OrObjectThatNeedConfigOnInit(new_config)
Built with Hugo | previoip (c) 2025