Exploring the Bunch Class


Warning

Be careful with how you use bunch classes. It is possible to overwrite critical methods and attributes. Please don’t use these in anything important or you may regret it.

Every play with a bunch class? I started using them early in my Python career, although it took four years to find out what they were called and better ways to code them.

Over time I became wary of them. In large and/or sophisticated projects you want predictable code, which explains the addition of the typing controls in Python. This goes against that direction, using the mutability of Python to expedite adding functionality while introducing potential instability.

Simple Unprotected Bunch Class

class Bunch:
    """
    Simple unprotected Python bunch class
    """

    def __init__(self, **kwargs):
        self.__dict__.update(kwargs)

bunch = Bunch(name='Exploring the bunch class')
print(bunch.name)

Those who don’t know about the Dict.update() method use this technique:

class VerboseBunch:
    """
    Simple, verbose unprotected Python bunch class
    """

    def __init__(self, **kwargs):
        for k, v in kwargs.items():
            self.__dict__[k] = v

We aren’t limited to __init__ methods, I’ve used a similar technique on update and save functions:

class Student:
    def __init__(self, **kwargs):
        self.__dict__.update(kwargs)
        self.grade_history = []
        self.current_grade = None

    def update(self, **kwargs):
        """Bunch style update!"""
        self.__dict__.update(kwargs)

    def grade(self):
        
        r = httpx.get(GRADING_SERVICE_URL)
        self.update(current_grade=r.json()['grade'])        
        self.grade_history.append(self.current_grade)

student = Student(name='Daniel', age=8)
studentgrad

Awesome, right?

Wrong, it’s not awesome. In fact, there’s a problem.

The Problem with Bunch Classes

Yes, bunch classes and generic update methods are fun, easy-to-understand, and take advantage of Python’s mutability. Unfortunately the mutability that powers Bunch classes can cause havoc. Here’s an example of a Bunch class failure:

student = Student(name='Daniel', age=8)

student.update(grade='A')

By updating the student’s grade, we have overwritten the grade() method.

Simple protected Python bunch class

You can make protected Bunch classes, that in theory don’t let pesky developers overwrite attributes, methods, and properties by accident during object instantiation.

class ProtectedBunch:
    """ 
    Use this when you don't want to overwrite
        existing methods and data
    """

    def __init__(self, **kwargs):
        for k, v in kwargs.items():
            if not hasattr(self, k):
                setattr(self, k, v)

bunch = ProtectedBunch(__str__=5)
print(bunch)

Note that this only protects the __init__ method. It doesn’t prevent any other method using the Bunch pattern. So overwriting the grade method is still possible if another method uses the bunch pattern.

Protected Python bunch class that throws errors

You can write bunch classes to raise errors when a key is in self.__dict__. This makes overrides explicitly fail.

class NotMutableAttribute(Exception):
    pass


class ProtectedBunchWithException:
    """ 
    Protected Python bunch class that throws errors

    Use this when you want to inform the coder
    they are touching protected code
    """
    def __init__(self, **kwargs):
        for k, v in kwargs.items():
            if hasattr(self, k):
                msg = f'{k} is not mutable'
                raise NotMutableAttribute(msg)
            else:
                setattr(self, k, v)

bunch = ProtectedBunch(__str__=5)
print(bunch)                

Again, this doesn’t block other methods using bunch-style dict updates from overriding methods and attributes.

Avoiding bugs caused by Bunch classes and bunch-style update methods

The best way to avoid the bugs generated by bunch classes or generic update methods that can overwrite critical methods and attributes is to simply avoid using them. If a project takes this route, I recommend putting it in the standards and conventions.

If you do go forward with bunch classes, don’t use checks of k not in self.__dict__ to block overriding existing methods. Methods and class attributes do not exist in the instance __dict__.

Historical Note

This is a modern update of a blog post I wrote nearly ten years ago.



Source link

Latest articles

Related articles

Leave a reply

Please enter your comment!
Please enter your name here