Can I hijack this post for a nit-pick? While I love Python and feel most comfortable with it, it always bothered me that it make a distinction between accessing a dictionary item and accessing an object member. E.g.: a["getStuff"]() # call the "getStuff" function of dictionary "a" b.getStuff() # call the "getStuff" method of object "b" In contrast, in JavaScript, these two are equivalant. a["getStuff"]() a.getStuff()…
In theory, implementing attribute-style access on user-defined Python object is just a matter of overriding its __(get|set)attr__ methods, for example:
In [1]: import collections
In [2]: class DotDict(collections.UserDict):
2 def __getattr__(self, k):
3 try:
4 return self.__getitem__(k)
5 except:
6 return super().__getattribute__(k)
7 def __setattr__(self, k, v):
8 try:
9 self.__setitem__(k, v)
10 except Exception as e:
11 super().__setattr__(k,v)
In [3]: d = DotDict({'a': 'b', 'c': [1,2,3], 'square': lambda x: x*x})
In [4]: d.square(4)
Out[4]: 16
In [5]: d
Out[5]: {'a': 'b', 'c': [1, 2, 3], 'square': at 0x7f9ef5d5b0d0>}
In [6]: import datetime
In [7]: d.created_at = datetime.datetime.now()
In [8]: d
Out[8]: {'a': 'b', 'c': [1, 2, 3], 'square': at 0x7f9ef5d5b0d0>, 'created_at': datetime.datetime(2021, 7, 19, 19, 42, 32, 341393)}
In practice, this can lead to headaches. Take, for example, what happens if we try to serialize `d` (which has a non-serializable lambda function in its keys) using the built-in pickle module: In [9]: import pickle
In [10]: pickle.dumps(d)
Traceback (most recent call last):
File "", line 1, in
_pickle.PicklingError: Can't pickle : attribute lookup DotDict on builtins failed
Thankfully there's a third-party library called cloudpickle which can serialize just about any python object to bytes -- even including the user-defined DotDict class (!): In [12]: deserialized_d = cloudpickle.loads(cloudpickle.dumps(d))
In [13]: deserialized_d
Out[13]: {'a': 'b', 'c': [1, 2, 3], 'square': at 0x7f9ef650fee0>, 'created_at': datetime.datetime(2021, 7, 19, 19, 42, 32, 341393)}
Since the entire class definition is serialized along with the instance, the deserialized copy preserves the object's attribute-style interface as before: In [14]: deserialized_d.created_at.strftime("%c")
Out[14]: 'Mon Jul 19 19:42:32 2021'
But I recommend Munch over rolling your own because subclassing `dict` is fraught with a surprising number of edge cases.