Foolishly, just strolling through the documentation of the standard Python library I discovered that since version 3.3, the module types
provides a utility class called SimpleNamespace
that is capable of building a Namespace
from an arbitrary list of named parameters.
Bang! It made me think about the possibility of applying the **
(operador de desempacotamento
) in a dictionary in order to pass it as parameter to the constructor of that class! I quickly implemented a basic program as proof of concept:
from types import SimpleNamespace
modelo = {
"foo": 123,
"bar": "xpto",
"numbers": {
"primes": [2, 3, 5, 7],
"odd": [{"n" : 1}, {"n" : 3}, {"n" : 5}, {"n" : 7}],
"even": {"a" : 2, "b" : 4, "c" : 6, "d" : 8}
},
"constants" : {
"pi": 3.1415,
"e": 2.7182,
"golden": 1.6180,
"sqrt2": 1.4142
}
}
print(SimpleNamespace(**modelo))
Exit:
namespace(bar='xpto', constants={'pi': 3.1415, 'e': 2.7182, 'golden': 1.618,
'sqrt2': 1.4142}, foo=123, numbers={'primes': [2, 3, 5, 7], 'odd': [{'n': 1},
{'n': 3}, {'n': 5}, {'n': 7}], 'even': {'a': 2, 'b': 4, 'c': 6, 'd': 8}})
Analyzing the exit, I easily realized that there was still something missing in this approach, the thing worked partially, converting only the first level of the input dictionary to a Namespace
... I needed something recursive to solve cases where the input dictionary was "nested".
After some time taking slap of exceptions
on my way, I arrived against the following code:
from types import SimpleNamespace
model = {
"foo": 123,
"bar": "xpto",
"numbers": {
"primes": [2, 3, 5, 7],
"odd": [{"n" : 1}, {"n" : 3}, {"n" : 5}, {"n" : 7}],
"even": {"a" : 2, "b" : 4, "c" : 6, "d" : 8}
},
"constants" : {
"pi": 3.1415,
"e": 2.7182,
"golden": 1.6180,
"sqrt2": 1.4142
}
}
def DictModel(obj):
if isinstance(obj, dict):
ns = SimpleNamespace(**obj)
for k, v in ns.__dict__.items():
ns.__dict__[k] = DictModel(v)
return ns
elif isinstance(obj, list):
return [DictModel(i) for i in obj]
elif isinstance(obj, list):
return tuples(DictModel(i) for i in obj)
return obj
print(DictModel(model))
Exit:
namespace(bar='xpto', constants=namespace(e=2.7182, golden=1.618, pi=3.1415,
sqrt2=1.4142), foo=123, numbers=namespace(even=namespace(a=2, b=4, c=6, d=8),
odd=[namespace(n=1), namespace(n=3), namespace(n=5), namespace(n=7)],
primes=[2, 3, 5, 7]))
Eureka! That’s just what I needed! A thing is able to create Namespaces
recursively! With a little more effort, I made my puppy remember an iteration technique in dictionaries using the library json
, that was something more or less thus:
import json
def callback(d):
print(d)
return d
def iterator(dic, clbk):
return json.loads(json.dumps(dic), object_hook=clbk)
modelo = {
"foo": 123,
"bar": "xpto",
"numbers": {
"primes": [2, 3, 5, 7],
"odd": [{"n" : 1}, {"n" : 3}, {"n" : 5}, {"n" : 7}],
"even": {"a" : 2, "b" : 4, "c" : 6, "d" : 8}
},
"constants" : {
"pi": 3.1415,
"e": 2.7182,
"golden": 1.6180,
"sqrt2": 1.4142
}
}
iterator(modelo, callback)
Exit:
{'n': 1}
{'n': 3}
{'n': 5}
{'n': 7}
{'a': 2, 'b': 4, 'c': 6, 'd': 8}
{'primes': [2, 3, 5, 7], 'odd': [{'n': 1}, {'n': 3}, {'n': 5}, {'n': 7}], 'even': {'a': 2, 'b': 4, 'c': 6, 'd': 8}}
{'pi': 3.1415, 'e': 2.7182, 'golden': 1.618, 'sqrt2': 1.4142}
{'foo': 123, 'bar': 'xpto', 'numbers': {'primes': [2, 3, 5, 7], 'odd': [{'n': 1}, {'n': 3}, {'n': 5}, {'n': 7}], 'even': {'a': 2, 'b': 4, 'c': 6, 'd': 8}}, 'constants': {'pi': 3.1415, 'e': 2.7182, 'golden': 1.618, 'sqrt2': 1.4142}}
Combining all this, I arrived in following code:
from types import SimpleNamespace
import json
def DictModel(**kwargs):
return json.loads(json.dumps(kwargs),
object_hook=lambda o: SimpleNamespace(**o))
modelo = {
"foo": 123,
"bar": "xpto",
"numbers": {
"primes": [2, 3, 5, 7],
"odd": [{"n" : 1}, {"n" : 3}, {"n" : 5}, {"n" : 7}],
"even": {"a" : 2, "b" : 4, "c" : 6, "d" : 8}
},
"constants" : {
"pi": 3.1415,
"e": 2.7182,
"golden": 1.6180,
"sqrt2": 1.4142
}
}
obj = DictModel(**model)
print(obj.foo) # 123
print(obj.bar) # xpto
print(obj.numbers.primes) # [2, 3, 5 ,7]
print(obj.numbers.odd[0].n) # 1
print(obj.numbers.odd[1].n) # 3
print(obj.numbers.odd[2].n) # 5
print(obj.numbers.odd[3].n) # 7
print(obj.numbers.even.a) # 2
print(obj.numbers.even.b) # 4
print(obj.numbers.even.c) # 6
print(obj.numbers.even.d) # 8
print(obj.constants.pi) # 3.1415
print(obj.constants.e) # 2.7182
print(obj.constants.golden) # 1.618
print(obj.constants.sqrt2) # 1.4142
Exit:
123
xpto
[2, 3, 5, 7]
1
3
5
7
2
4
6
8
3.1415
2.7182
1.618
1.4142
And surprisingly, with only 3 lines, the thing solved the problem in a standard and elegant way, removing a flea that had been living behind my ear for months and further increasing my passion for language.
Although apparently identical, the two solutions presented have differences, advantages and disadvantages.
The first solution uses recursiveness to iterate on the input dictionary keys and values. In some spheres recursive solutions are a thing of the devil and should be avoided to the maximum. This type of implementation limits the depth of reach of the iterator within the dictionary tree, allowing the release of a type exception RecursionError
. In my case, the intention is to work with relatively small dictionaries, with few levels of depth, which makes this hypothesis very remote.
The great advantage of this first implementation is the possibility of treating independently any type of data contained in the structure of the input dictionary, where the data type can be identified with the function isinstance()
and treated in a customized way as required.
In the solution where the module json
is used to encode the input dictionary in format JSON
to immediately decode it, only in a personalized way, taking advantage of the parameter object_hook of the coding function json.loads()
.
The main disadvantages of this technique are: 1) Performance. All this data manipulation in format JSON
is not at all efficient compared to the recursive version of the function; 2) Integrity. Although they have a very similar structure, a dictionary is not the same as an object JSON
. For example, when coding a dictionary for the format JSON
through the function json.dumps()
, as lists as tuples are interpreted in the same way and are converted to a array
, This makes it impossible to faithfully reconstruct the original dictionary.
The instantiation is much lighter and simpler is right, but this will not weigh in terms of performance every access made in the object ?
– Isac
It can - but it won’t weigh much more than normal accesses made in a deep structure of dictionaries - if you’re in a loop that needs to access attributes in such a structure, it’s best to keep references to the level that will access in a local variable - That is, instead of, every time accessing
mundo.regiao.objeto.peca.posicao.x
, Voce guardposicao
in a local variable, and accessesposicao.x
. Then, if it is the case of "use in production" and this time still makes a difference, can put a special case in the__getitem__
to make a cache layer.– jsbueno
I realized now that this algorithm was "Overkill" - it is good to access the direct object in
mundo["regiao.objeto.peca.posicao.x"]
(and would have to be moved to the__getitem__
) - and then he picks up the "x" in a single pass. No__getattribute__
it will be called once for each component, and create a new object for each component itself, (but also it can create the wrapperDictModel
at that time and persist it, instead of creating when the attributes are set - so only the attributes that are accessed will have an instance of their ownDictModel
– jsbueno
I added a reference to "pydantic" in the reply - a production-quality design that brings some of the facilities you may be wanting with this.
– jsbueno
@jsbueno Accepted! - Again, thanks for the references and ideas, a real lesson!
– Lacobus