Naming
The name of __genereate_initial_guess()
starts with a double underscore. Since it is not a method, it will not get mangled with a class name anyway. I'd suggest to remove the underscores.
Then the name of your function does not convey in any way what it's doing. It is not generating a guess in any way. It's extracting parameters and limits.
Documentation
The function's name is a segue to good documentation. Use docstrings to briefly describe what your functions are doing.
Divide and conquer
Your function __genereate_initial_guess()
does way too much, that can be divided into separate functions, each dealing with part of the issue at hand.
Know your datatypes
The built-in dict
has a method items()
to iterate over its key-value pairs. Use it.
Consistency
Be consistent. If you use type hints, why only for the return value?
Generalize
On the bottom level of your dict processing, the only difference lies between extracting the paramters and their limits. You can use a callback function to process the leaves of your dict, so you don't have to write the superordinate iteration functions twice.
Suggested change
from typing import Callable, Iterator, NamedTuple, Union
Number = Union[int, float]
Limits = tuple[Number, Number]
T = Union[Number, Limits]
class ParamsAndLimits(NamedTuple):
"""Parameters and limits of the data structure."""
params: dict[str, Number]
limits: dict[str, Limits]
def process_kind(kind: list, callback: Callable) -> Iterator[dict[str, T]]:
"""Yield dicts for each kind."""
for line in kind:
yield dict(process_line(line, callback))
def process_line(line: list, callback: Callable) -> Iterator[tuple[str, T]]:
"""Yield items for each line."""
for item in filter(lambda item: item.get('use'), line):
yield from callback(item)
def get_params(item: dict) -> Iterator[tuple[str, Number]]:
"""Yield parameters of the item."""
for key in filter(lambda key: key not in {'limits', 'use'}, item):
yield key, item[key]
def get_limits(item: dict) -> Iterator[tuple[str, tuple[Number, Number]]]:
"""Yield limits of the item."""
for key in filter(lambda key: key not in {'limits', 'use'}, item):
yield key, tuple(item['limits'])
def get_parameters_and_limits(dataset: dict) -> ParamsAndLimits:
"""Return one dict with parameters and one with limits."""
params = {}
limits = {}
for key, kind in dataset.items():
params[key] = list(process_kind(kind, get_params))
limits[key] = list(process_kind(kind, get_limits))
return ParamsAndLimits(params, limits)
example = {
"main" : [
[
{"A" : 0, "limits" : [0, 0.1], "use" : 1},
{"B" : 0.01, "limits" : [-0.1, 0.1], "use" : 1},
{"C" : 0, "limits" : [0, 1], "use" : 0}
],
[
{"A" : 0, "limits" : [0, 0.1], "use" : 1},
{"B" : 0.01, "limits" : [-0.1, 0.1], "use" : 1},
{"C" : 0, "limits" : [0, 1], "use" : 1}
]
],
"background" : [
[
{"A" : 0, "limits" : [0, 0.1], "use" : 1},
{"B" : 0.01, "limits" : [-0.1, 0.1], "use" : 1}
]
]
}
params, limits = get_parameters_and_limits(example)
print(params, limits)
Alternative without callbacks
Since the above code still has some duplication and iterates over the source dict twice, you can also replace the callback approach by extracting the parameters and limits in one go. The disadvantage of this is, that you, in the end, still have to loop over the results twice due to the nature of your desired output datastructure. It also makes the functions a bit more complex, since the extraction function will do two things at once.
from typing import Iterator, NamedTuple, Union
Number = Union[int, float]
Limits = tuple[Number, Number]
class ExtractedParams(NamedTuple):
"""Extracted parameters."""
key: str
value: Number
limits: Limits
class ParamsAndLimits(NamedTuple):
"""Parameters and limits of the data structure."""
params: dict[str, Number]
limits: dict[str, Limits]
def process_kind(kind: list) -> Iterator[ParamsAndLimits]:
"""Yield params and limits for each kind."""
for line in kind:
params = {}
limits = {}
for extracted_params in process_line(line):
params[extracted_params.key] = extracted_params.value
limits[extracted_params.key] = extracted_params.limits
yield ParamsAndLimits(params, limits)
def process_line(line: list) -> Iterator[ExtractedParams]:
"""Yield items for each line."""
for item in filter(lambda item: item.get('use'), line):
for key in filter(lambda key: key not in {'limits', 'use'}, item):
yield ExtractedParams(key, item[key], tuple(item['limits']))
def get_parameters_and_limits(dataset: dict) -> ParamsAndLimits:
"""Return one dict with parameters and one with limits."""
params = {}
limits = {}
for key, kind in dataset.items():
params_and_limits = list(process_kind(kind))
params[key] = [p.params for p in params_and_limits]
limits[key] = [p.limits for p in params_and_limits]
return ParamsAndLimits(params, limits)
example = {
"main" : [
[
{"A" : 0, "limits" : [0, 0.1], "use" : 1},
{"B" : 0.01, "limits" : [-0.1, 0.1], "use" : 1},
{"C" : 0, "limits" : [0, 1], "use" : 0}
],
[
{"A" : 0, "limits" : [0, 0.1], "use" : 1},
{"B" : 0.01, "limits" : [-0.1, 0.1], "use" : 1},
{"C" : 0, "limits" : [0, 1], "use" : 1}
]
],
"background" : [
[
{"A" : 0, "limits" : [0, 0.1], "use" : 1},
{"B" : 0.01, "limits" : [-0.1, 0.1], "use" : 1}
]
]
}
params, limits = get_parameters_and_limits(example)
print(params, limits)
dict.items()
instead. This will also scale for additional keys. \$\endgroup\$