Quickstart¶
In real world projects, maintaining a large amount of parameters for user inputs/options, function input variables and program runtime status is almost inevitable. A clear, robust and extensible design of parameter structures, rules and restrictions greatly contributes to the quality of project codes. Paramdict provides a Pythonic way to achieve this goal: parameters are stored in a dictionary, wrapped simply with a class and several decorators.
What is a Paramdict¶
A paramdict defines a specific organization of a set of parameters: what is the type of the parameter, whether to strictly check the type, can this parameter be left blank, is there any limit about the parameter’s range, etc. Following is an exmaple:
[1]:
from paramdict import *
class pdTest(baseParamDict):
def __init__(self, id:str='', source=None):
super().__init__(id, source)
def _setcls(self):
return 'pdtest'
def _pdinit(self):
pass
def _check(self):
return super()._check()
@pSimple
def a(self) -> int: ...
@pSimple
def b(self) -> float: ...
@pStrict
def c(self) -> str: ...
@pList
def d(self) -> list[int]: ...
Here, a paramdict class named “pdtest” with 4 parameters, “a”, “b”, “c”, and “d”, is defined. Any paramdict instance of this class will be able to and only able to contain these 4 parameters. The decorators, pSimple, pStrict, and pList, works like Python’s built-in property decorator, except that setting method is implemented automatically. So to set or get a parameter:
[2]:
pd1 = pdTest()
# set values
pd1.a = 1
pd1.b = 2.0
pd1.c = '3'
pd1.d = [4, 5, 6]
# get values
print(f"a: {pd1.a}")
print(f"b: {pd1.b}")
print(f"c: {pd1.c}")
print(f"d: {pd1.d}")
a: 1
b: 2.0
c: 3
d: [4, 5, 6]
Annotations matters for the decorated parameter methods. When setting a parameter decorated by pSimple, the input value will be automatically converted to the annotated type:
[3]:
pd1.a = 3.0
print(f"a: {pd1.a}")
print(f"Type of a: {type(pd1.a)}")
a: 3
Type of a: <class 'int'>
pStrict behaves differently from pSimple. It checks the type of input value, and raise errors when input type is not the annotated type and does not inherit from the annotated type:
[4]:
pd1.c = 1
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[4], line 1
----> 1 pd1.c = 1
File ~/utils/paramdict/paramDictProperties.py:79, in pStrict.__set__(self, instance, value)
77 aType:type = get_type_hints(self.fget)['return']
78 if not isinstance(value, aType):
---> 79 raise TypeError(_('Property %s must be of type %s.') % (outYellow('\''+self.fget.__name__+'\''), outGreen(aType)))
80 instance._cont[self.fget.__name__] = value
TypeError: Property 'c' must be of type <class 'str'>.
pList not only checks if the input value is a list, but also if all the elements are of the right type.
[5]:
pd1.d = [4, 5, '6']
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[5], line 1
----> 1 pd1.d = [4, 5, '6']
File ~/utils/paramdict/paramDictProperties.py:110, in pList.__set__(self, instance, value)
108 for item in value:
109 if not isinstance(item, aType):
--> 110 raise TypeError(_('Property %s must be of type %s.') % (outYellow('\''+self.fget.__name__+'\''), outGreen(List[aType])))
111 instance._cont[self.fget.__name__] = value
TypeError: Property 'd' must be of type typing.List[int].
Paramdict Nested in Paramdict¶
An important trait of paramdict is that a paramdict can be nested in another paramdict while maintaining all the functionality:
[6]:
from copy import deepcopy
class pdTest2(baseParamDict):
def __init__(self, id:str='', source=None):
super().__init__(id, source)
def _setcls(self):
return 'pdtest2'
def _pdinit(self):
pass
def _check(self):
return super()._check()
@pPd
def param_pdtest(self) -> pdTest: ...
@pPdList
def param_pdtest_list(self) -> list[pdTest]: ...
pd2 = pdTest2()
pd2.param_pdtest = deepcopy(pd1)
pd2.param_pdtest.a = 10
pd2.param_pdtest_list = [deepcopy(pd1), deepcopy(pd1)]
pd2.param_pdtest_list[0].b = 20.0
pd2.param_pdtest_list[1].c = '30'
pd2.content
[6]:
{'class': 'pdtest2',
'id': '',
'param_pdtest': {'class': 'pdtest',
'id': '',
'a': 10,
'b': 2.0,
'c': '3',
'd': [4, 5, 6]},
'param_pdtest_list': [{'class': 'pdtest',
'id': '',
'a': 3,
'b': 20.0,
'c': '3',
'd': [4, 5, 6]},
{'class': 'pdtest', 'id': '', 'a': 3, 'b': 2.0, 'c': '30', 'd': [4, 5, 6]}]}
This paramdict class contains two parameters: “param_pdtest” is a paramdict that follows pdTest paramdict class definition, and “param_pdtest_list” is a list that contains paramdicts that also follows pdTest. Then a paramdict instance (pd2) is initialized and modified. By referring the content built-in property, we can check the dictionary where values are actually stored.
Persistence¶
It is also easy to serialize the content:
[7]:
import json
json_str = json.dumps(pd2.content, indent=4)
print(json_str)
{
"class": "pdtest2",
"id": "",
"param_pdtest": {
"class": "pdtest",
"id": "",
"a": 10,
"b": 2.0,
"c": "3",
"d": [
4,
5,
6
]
},
"param_pdtest_list": [
{
"class": "pdtest",
"id": "",
"a": 3,
"b": 20.0,
"c": "3",
"d": [
4,
5,
6
]
},
{
"class": "pdtest",
"id": "",
"a": 3,
"b": 2.0,
"c": "30",
"d": [
4,
5,
6
]
}
]
}
Or to deserialize:
[8]:
pdcont = json.loads(json_str)
pd3 = pdTest2(source=pdcont)
pd3.content
[8]:
{'class': 'pdtest2',
'id': '',
'param_pdtest': {'class': 'pdtest',
'id': '',
'a': 10,
'b': 2.0,
'c': '3',
'd': [4, 5, 6]},
'param_pdtest_list': [{'class': 'pdtest',
'id': '',
'a': 3,
'b': 20.0,
'c': '3',
'd': [4, 5, 6]},
{'class': 'pdtest', 'id': '', 'a': 3, 'b': 2.0, 'c': '30', 'd': [4, 5, 6]}]}