Channels ▼
RSS

Parallel

Taming Python


The LockAttributesMetaclass

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 LockAttributeMixin

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[0])
    self.radius = state[1]
    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)

Conclusion

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.


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.
 

Video