Advertisement
Guest User

frozenclass

a guest
Dec 10th, 2013
789
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 8.39 KB | None | 0 0
  1. #!/usr/bin/env python
  2. # -*- coding: UTF-8 -*-
  3. """ Define the @frozenclass class decorator.
  4.  
  5.    The @frozenclass decorator provides capabilities to permit Python classes
  6.    to be used in non-traditional ways as flexible data containers.
  7.  
  8.    Examples include:
  9.  
  10.    1. Enums:
  11.  
  12.    Python users often implement enums using the pattern of class definitions
  13.    containing only initialized class attributes (no methods).  This pattern has
  14.    a problem if the class is instantiated: The class attributes become mutable!
  15.  
  16.    Adding @frozenclass before the class makes class attributes immutable when
  17.    the class is instantiated.
  18.  
  19.        @frozenclass
  20.        class my_enum:
  21.            MY_ENUM = 42    # A field with a value that can't be changed
  22.  
  23.    2. Records:
  24.  
  25.    Records are data structures containing a fixed set of named variables.
  26.    Python has no native capability like this: Python dictionaries provide
  27.    named fields, but the set of fields is mutable.  Python provides an
  28.    immutable record facility via collections.namedtuple that implements a
  29.    fixed set of fields, but their values are constant.  Python frozendict
  30.    (via frozenset) is similar, but has limitations similar to namedtuples.
  31.  
  32.    @frozenclass combines enums and records into a far more flexible pattern.
  33.    Immutable fields are declared as class attributes.  Mutable fields (instance
  34.    attributes) are created only by __init__().  No new attributes can  be added
  35.    after instantiation, and only instance attributes (not class attributes) may
  36.    have their values updated after instantiation.
  37.  
  38.        @frozenclass
  39.        class my_record(object):
  40.            def __init__():
  41.                self.aField = 42    # A mutable field with a default init
  42.  
  43.    The user must craft an __init__() function if mutable attributes are
  44.    desired.  Optionally, __init__() may have parameters to permit flexible
  45.    attribute initialization during instantiation.  Any attribute may be
  46.    initialized in this manner.
  47.  
  48.        @frozenclass
  49.        class my_record(object):
  50.            aConst = 15
  51.            def __init__(aField=None):
  52.                self.aField = aField    # A field with an overridable default
  53.  
  54.        @frozenclass
  55.        class my_record(object):
  56.            aNonconfigurableConst = 12  # Not affected by __init__() below
  57.            aConfigurableConst = 25     # This value is always stepped on!
  58.  
  59.            def __init__(aConfigurableConst = 42, aField=None):
  60.                self.aConfigurableConst = aConfigurableConst
  61.                self.aField = aField
  62.  
  63.    The @frozenclass decorator ALWAYS prevents adding new attributes after
  64.    instantiation.  It ALWAYS prevents changes to class attribute values after
  65.    instantiation.  It OPTIONALLY permits any attribute (both class and
  66.    instance attributes) to be initialized as part of instantiation, if the
  67.    user adds that capability as parameters to and code within the definition
  68.    of the class __init__() function.
  69.  
  70.    CAVEAT: @frozenclass uses super() when __init__() is present, so your class
  71.            will need to be a "new style" class.  You will need to explicitly
  72.            inherit from 'object' if you see this error:
  73.  
  74.                TypeError: must be type, not classobj
  75.  
  76.    Neither inheriting from object nor defining __init__() is needed for
  77.    enum-only @frozenclasses.
  78.  
  79.    3. "Smart" Records:
  80.  
  81.    The @frozenclass decorator is primarily intended to provide structures with
  82.    varying degrees of field constness.  But it can also do more, just like any
  83.    other regular Python class.
  84.  
  85.  
  86.    FUTURE:  The present implementation is minimal, lacking desirable features:
  87.  
  88.    - The ability to export attributes and values as a dictionary or namedtuple.
  89.      Status: Implemented in the separate asdict.py module.
  90.  
  91.    - Since the number of fields is fixed, attribute ordering can be supported,
  92.      along with indexing, slicing, and iteratators.
  93.      Status: For now, use functions in asdict to obtain similar behavior.
  94.  
  95.    - The __init__() function is boilerplate: It should be generated by the
  96.      decorator. This could be done by making this a function decorator, where
  97.      function parameters (with optional defaults) become mutable attributes,
  98.      and function locals become immutable attributes.  The decorated function
  99.      would then become a 'factory function' similar to that created by
  100.      collections.namedtuple.
  101.      Status:  Perhaps a 'namedclass' facility?
  102.  
  103.    - New mutable instance attributes may be created during instantiation by
  104.      defining __init__() as follows:
  105.  
  106.          def __init__(self, *args, **kwargs):
  107.              for key,val in kwargs.items():
  108.                  setattr(self, key, val)
  109.  
  110.      Status: It should be possible for the decorator to provide this service for
  111.      enum-style classes (no __init__() and no inheritance), but I haven't found
  112.      a way to make it work within the decorator.  It works fine for the user.
  113. """
  114.  
  115. def frozenclass(cls):
  116.     """ A decorator to modify a class so that, after instantiation, class
  117.        attribute values cannot be changed, and instance attributes cannot
  118.        be created (but existing instance attribute values may be modified).
  119.    """
  120.     if hasattr(cls, '__init__'):
  121.         _init = cls.__init__
  122.         def init(self, *args, **kw):
  123.             _init(self, *args, **kw)
  124.             self.freeze = True
  125.     else:
  126.         def init(self, *args, **kw):
  127.             self.freeze = True
  128.     cls.__init__ = init
  129.  
  130.     def _setattr(self, attr, value):
  131.         if getattr(self, "freeze", None):   # Are we an initialized instance?
  132.             # The attribute namespace is frozen:
  133.             if attr=="freeze" or not hasattr(self, attr):
  134.                 raise AttributeError("You shall not create attributes!")
  135.             # Class attributes are immutable:
  136.             if hasattr(type(self), attr):
  137.                 raise AttributeError("You shall not modify immutable attributes!")
  138.         super(cls, self).__setattr__(attr, value)
  139.     cls.__setattr__ = _setattr
  140.  
  141.     return cls
  142.  
  143.  
  144.  
  145. if __name__ == "__main__":
  146.     # Let's test this puppy:
  147.  
  148.     @frozenclass
  149.     class myClass(object):
  150.         """ A demo class."""
  151.         # The following are immutable after init:
  152.         a = None; b = None; c = None
  153.  
  154.         def __init__(self, a, b, c, d=None, e=None, f=None):
  155.             # We don't need to pass in initializers if all we want is to make
  156.             # the class attribute values be true consts, even when instantiated.
  157.             self.a = a; self.b = b; self.c = c  # Immmutable after init
  158.             self.d = d; self.e = e; self.f = f  # Mutable
  159.  
  160.     # Tests
  161.     try:
  162.         a = myClass()                       # No init parameters
  163.     except Exception, e:
  164.         print "Expected error:", e
  165.  
  166.     try:
  167.         a = myClass(1, 2)                   # Too few init parameters
  168.     except Exception, e:
  169.         print "Expected error:", e
  170.  
  171.     try:
  172.         a = myClass(1, 2, 3, 4, 5, 6, 7)    # Too many init parameters
  173.     except Exception, e:
  174.         print "Expected error:", e
  175.  
  176.     print "No error expected for correct partial initialization."
  177.     a = myClass(1, 2, 3)
  178.  
  179.     print "No error expected for correct full initialization."
  180.     a = myClass(1, 2, 3, 4, 5, 6)
  181.  
  182.     try:
  183.         a.b = 5                             # Attempt to assign immutable
  184.     except Exception, e:
  185.         print "Expected error:", e
  186.  
  187.     print "No error expected when modifying mutable attributes."
  188.     a.e = 5
  189.  
  190.     try:
  191.         a.g = "Fail!"                       # Try to create instance attribute
  192.     except Exception, e:
  193.         print "Expected error:", e
  194.  
  195.  
  196.     @frozenclass
  197.     class aClass(object):
  198.         AN_ENUM = 15
  199.         ANOTHER_ENUM = 25
  200.  
  201.         def __init__(self, *args, **kwargs):
  202.             for key,val in kwargs.items():
  203.                 setattr(self, key, val)
  204.  
  205.     print "No error expected when adding no dynamic attributes."
  206.     q = aClass()
  207.     #print q.AN_ENUM, q.ANOTHER_ENUM
  208.     print "No error expected when overriding class attribute initialization."
  209.     r = aClass(AN_ENUM=16)
  210.     #print r.AN_ENUM, r.ANOTHER_ENUM
  211.     print "No error expected when adding a dynamic attribute."
  212.     s = aClass(myField = 15)
  213.     #print s.AN_ENUM, s.ANOTHER_ENUM, s.myField
  214.     print "No error expected when updating a dynamic attribute."
  215.     s.myField = 16
  216.     #print s.AN_ENUM, s.ANOTHER_ENUM, s.myField
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement