Our bunch implementation is not yet complete, as it will fail any test for class name (it's always named Bunch) and any test for inheritance, thus failing at faking other objects.
The first step is to make Bunch able to shapeshift not only its properties, but also its name. This can be achieved by creating a new class dynamically every time we create Bunch. The class will inherit from Bunch and will do nothing apart from providing a new name:
>>> class BunchBase(dict):
... def __getattribute__(self, key):
... try:
... return self[key]
... except KeyError:
... raise AttributeError(key)
...
... def __setattr__(self, key, value):
... self[key] = value
...
>>> def Bunch(_classname="Bunch", **attrs):
... return type(_classname, (BunchBase, ), {})(**attrs)
>>>
The Bunch function moved from being the class itself to being a factory that will create objects that all act as Bunch, but can have different classes. Each Bunch will be a subclass of BunchBase, where the _classname name can be provided when Bunch is created:
>>> b = Bunch("Request", path="/index.html", host="www.example.org")
>>> print(b)
{'path': '/index.html', 'host': 'www.example.org'}
>>> print(b.path)
/index.html
>>> print(b.host)
www.example.org
This will allow us to create as many kinds of Bunch objects as we want, and each will have its own custom type:
>>> print(b.__class__)
<class '__main__.Request'>
The next step is to make our Bunch actually look like any other type that it has to impersonate. That is needed for the case where we want to use Bunch in place of another object. As Bunch can have any kind of attribute, it can take the place of any kind of object, but to be able to, it has to pass type checks for custom types.
We need to go back to our Bunch factory and make the Bunch objects not only have a custom class name, but also appear to be inherited from a custom parent.
To better understand what's going on, we will declare an example Person type; this type will be the one our Bunch objects will try to fake:
class Person(object):
def __init__(name, surname):
self.name = name
self.surname = surname
@property
def fullname(self):
return '{} {}'.format(self.name, self.surname)
Specifically, we are going to print Hello Your Name through a custom print function that only works for Person:
def hello(p):
if not isinstance(p, Person):
raise ValueError("Sorry, can only greet people")
print("Hello {}".format(p.fullname))
We want to change our Bunch factory to accept the class and create a new type out of it:
def Bunch(_classname="Bunch", _parent=None, **attrs):
parents = (_parent, ) if parent else tuple()
return type(_classname, (BunchBase, ) + parents, {})(**attrs)
Now, our Bunch objects will appear as instances of a class named what we wanted, and will always appear as a subclass of _parent:
>>> p = Bunch("Person", Person, fullname='Alessandro Molina')
>>> hello(p)
Hello Alessandro Molina
Bunch can be a very convenient pattern; in both its complete and simplified versions, it is widely used in many frameworks with various implementations that all achieve pretty much the same result.
The showcased implementation is interesting because it gives us a clear idea of what's going on. There are ways to implement Bunch that are very smart, but might make it hard to guess what's going on and customize it.
Another possible way to implement the Bunch pattern is by patching the __dict__ class, which contains all the attributes of the class:
class Bunch(dict):
def __init__(self, **kwds):
super().__init__(**kwds)
self.__dict__ = self
In this form, whenever Bunch is created, it will populate its values as a dict (by calling super().__init__, which is the dict initialization) and then, once all the attributes provided are stored in dict, it swaps the __dict__ object, which is the dictionary that contains all object attributes, with self. This makes the dict that was just populated with all the values also the dict that contains all the attributes of the object.
Our previous implementation worked by replacing the way we looked for attributes, while this implementation replaces the place where we look for attributes.