This meta-class has three goals:
- If the lock attributes machinery is deactivated just bail out early
- Check each attribute setting to verify that the lock atributes principle is not violated.
- Decorate the __init__() and __setstate__() methods with the _allow_new_attributes decorator
It accomplishes the first goal simply by checking if a special environment variable, the deactivation_key (DONT_USE_LOCK_ATTRIBUTES) is in the environment:
class LockAttributesMetaclass(type): def __init__(cls, name, bases, dict): # Bail out if not active. Zero overhead other than this one-time check # at class definition time if deactivation_key in os.environ: return
It accomplishes the second goal by setting the class __setattr__() method to a custom one that checks for lock attributes violations (described later) and also keeping the original __setattr__ in an attribute called _original_setattr (that will be used later):
# Initialize the super-class type.__init__(cls, name, bases, dict) # Store and replace the __setattr__ with the custom one (if needed) if not hasattr(cls, '_original_setattr'): cls._original_setattr = cls.__setattr__ cls.__setattr__ = custom_setattr
It accomplishes the third goal by looking up the __init__() and __setstate__() methods and decorating them with the _allow_new_attributes decorator. There is a subtle point here, which is that the target class might not have __init__() or __setstate__() methods. Well, if there is no __setstate__() it's okay because even if an instance is loaded from a pickle the default __setstate__ will not add any new attributes. But, if there is no __init__() method it can be a problem. The reason is that the lock attributes checking supports inheritance. If a subclass of an attribute locked class is trying to create new attributes in its __init__() method it should be able to do it. So, if there is no __init__() method a trivial __init__() method is added (described later):
# Get the __init__ and __setstate__ from the target class's dict # If there is no __init__ use _simple_init (it's Ok if there is no #__setstate__) methods = [('__init__', dict.get('__init__', _simple_init)), ('__setstate__', dict.get('__setstate__', None))] # Wrap the methods with _allow_new_attributes decorator for name, method in methods: if method is not None: setattr(cls, name, _allow_new_attributes(method))
Here is the _simple_init() function:
def _simple_init(self, *args, **kw): """Trivial init method that just calls the base class' __init__() This method is attached to classes that don't define __init__(). It is needed because LockAttributesMetaclass must decorate the __init__() method of its target class. """ type(self).__base__.__init__(self, *args, **kw)
The custom_setattr Function
This function allows setting only existing attributes. It is designed to work with the _allow_new_attributes decorator. It works is by checking if the requested attribute is already in the __dict__ or if the _canAddAttributes counter > 0; otherwise it raises an exception.
If all is well it calls the original __setattr__(). This means it can work also with classes that already have custom __setattr__:
def custom_setattr(self, name, value): if (name == '_canAddAttributes' or (hasattr(self, '_canAddAttributes') and self._canAddAttributes > 0) or hasattr(self, name)): return self._original_setattr(name, value) else: raise Exception('Attempting to set a new attribute: ' + name)
The LockAttributesMixin just attaches the LockAttributesMetaclass to the target class. The term "mixin" means a class that is intended to be used as a base class and provide some functionality to classes that derive from it, but it is not useful by itself, so it should never be instantiated directly. The mixin often cooperates with the derived class or other mixin classes:
class LockAttributesMixin(object): """This class serves as a base (or mixin) for classes that want to enforce the locked attributes pattern (all attributes should be defined in __init__() or __setstate__(). All the target class has to do add LockAttributesMixin as one of its bases (inherit from it). The meta-class will be activated when the application class is created and the lock attributes machinery will be injected (unless the deactivation_key is defined in the environment) """ __meta-class__ = LockAttributesMetaclass
Complete Example with Inheritance and Persistance
The following example demonstrates the lock attributes mechanism in the context of a class hierarchy where both the base class and the derived class implements __getstate__()/__setstate__() for persistence into pickle files. The pickle module allows storing and loading Python objects in files. When the object is saved using pickle.dump(), the __getstate__() method is called and its return value is stored in the file. When the object is loaded using pickle.load(), the __setstate__() method is called with the same state that __getstate__() returned and the object can initialize itself from this state.
Here is the base class Shape with the required import statements. Note that the state the Shape class manages is simply the center point. It subclasses the LockAttributesMixin, which means this class and all its subclasses will automatically benefit from locked attributes enforcement.
from lockattributes import LockAttributesMixin import math import cPickle as pickle class Shape(LockAttributesMixin): def __init__(self, center): self.center = center def __getstate__(self): return self.center def __setstate__(self, state): self.center = state
The Circle class subclasses the Shape class and has two more attributes --radius and area -- that are set in __init__() and __setstate__(). Note that the area is NOT stored in the pickle file to save space because it can be calculated from the radius. The Circle subclass plays nicely with its base class and makes sure to call its __init__(), , and __setstate__() at the right time. The state the Circle manages is a tuple that contains the state of the Shape class as first member and the radius as the second member. This is a pretty robust scheme because the base class may add or change its persistent state and the subclass persistence code will not need to change:
class Circle(Shape): def __init__(self, center, radius): Shape.__init__(self, center) self.radius = radius self.calculateArea() def calculateArea(self): self.area = math.pi * math.pi * self.radius def calculatePerimeter(self): self.perimeter = 2 * math.pi * self.radius def __getstate__(self): return (Shape.__getstate__(self), self.radius) def __setstate__(self, state): Shape.__setstate__(self, state) self.radius = state self.calculateArea()
Here is some code that creates a Circle object, displays its attributes and then calls the forbidden calculatePerimeter() method that attempts to create a new 'perimeter' attribute:
# Create a new circle x = Circle((3, 4), 5) # Display its attributes print 'center:', x.center print 'radius:', x.radius print 'area:', x.area # Call the calculatePerimeter method that creates a new attribute try: x.calculatePerimeter() except Exception as e: print str(e)
The next snippet is similar except that the x object is stored in a pickle file and then loaded into a new instance x2:
# Store the circle in a pickle file with open('circle.pkl', 'w') as f: pickle.dump(x, f) # Load the circle from the .pkl file with open('circle.pkl') as f: x2 = pickle.load(f) # Display its attributes print 'center:', x2.center print 'radius:', x2.radius print 'area:', x2.area # Call the calculatePerimeter method that creates a new attribute try: x2.calculatePerimeter() except Exception as e: print str(e)
In this article, I have demonstrated the amazing flexibility of Python and the tools it provides to developers to alter its own rules when necessary. As you know power corrupts and ultimate power corrupts ultimately. Don't let the power Python puts in your hands corrupt your code. Used judiciously, you can do great things with Python. Used indiscriminately, you end up with a bad case of obfuscated spaggheti code and collegues hiding under their desks when they hear your footfalls.