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):
# Determines grade of student
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)
# Overriding the grade() method by accident
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.
from Planet Python
via read more
No comments:
Post a Comment