ActiveState Code

Recipe 197965: Simple read only attributes with meta-class programming


This recipe is a rewrite of a portion of http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/157768 . It shows an easy way to write read-only attributes with the help of meta classes.

Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# simple read only attributes with meta-class programming

# method factory for an attribute get method
def getmethod(attrname):
    def _getmethod(self):
        return self.__readonly__[attrname]

    return _getmethod

class metaClass(type):
    def __new__(cls,classname,bases,classdict):
        readonly = classdict.get('__readonly__',{})
        for name,default in readonly.items():
            classdict[name] = property(getmethod(name))

        return type.__new__(cls,classname,bases,classdict)

class ROClass(object):
    __metaclass__ = metaClass
    __readonly__ = {'a':1,'b':'text'}


if __name__ == '__main__':
    def test1():
        t = ROClass()
        print t.a
        print t.b

    def test2():
        t = ROClass()
        t.a = 2

    test1()

Discussion

This example shows that even a few lines of code (in this case the definition of the meta class) can save quite a lot of "classic", handwritten code.

The main idea behind this example is the automatic creation of property methods and the binding of an attribute to them.

Further reading: Python in a Nutshell by Alex Martelli The sources of SQLObject (http://sqlobject.org/) by Ian Bicking

Comments

  1. 1. At 1:53 p.m. on 8 may 2003, Sean Ross said:

    This a very good solution. I like it at least as much as my own.

    Sean Ross

  2. 2. At 5:37 p.m. on 8 may 2003, Raymond Hettinger said:

    Simplified a bit and contrasted with non-metaclass programming. Nice example of how metaclasses work.

    It can be simplified a bit by eliminating the nested getmethod() helper function and replacing for-loop with:

    for name,default in readonly.items():
        classdict[name] = property(lambda self, x=default:x)
    

    Though it is an instructive metaclass example, it isn't actually an improvement over non-metaclass code. The following is shorter, clearer, simpler, and faster.

    def readonly(value):
        return property(lambda self: value)
    
    class ROClass(object):
        a = readonly(1)
        b = readonly('text')
    
  3. 3. At 7:13 p.m. on 8 may 2003, Sean Ross said:

    One shortcoming of this solution is that the read-only properties that are created are not associated with any instance attributes. Generally, the reason you would use a property is so that you can control how the values of your instance attributes are exposed to users of your classes. Since your read-only properties have no instance attribute association, what's being created here are, essentially, class constants. This is fine, if this is all you're looking for. However, you are losing part of the benefit of having a read-only property.

    By creating your read-only properties with an association to an instance attribute you remain able to change the value that property will return. For example, if you have a read-only property foo and it is associated with a private instance variable __foo, i.e. instance.foo returns the value of instance._classname__foo, then if the value of __foo is changed by some internal operation, say in some other method call, then the value returned by subsequent calls to instance.foo will also change. In other words, instance.foo is no longer just a constant, it's read-only, and these two things are not the same.

    Of course, you could change the value of instance.__readonly__['foo'], internally, to accomplish the above behaviour. Still, why not just have instance.__foo available, if you want/need it?

  4. 4. At 1:51 p.m. on 23 jul 2003, Michele Simionato said:

    Make __new__ cooperative.

    It is better to replace
    
    type.__new__(cls,classname,bases,classdict)
    
    with
    
    super(metaClass,cls).__new__(cls,classname,bases,classdict)
    
    in the case you anticipate to compose your metaclass
    with another metaclass one via multiple inheritance. Also,
    I like to call the first argument of the metaclass __new__
    method "meta" and not "cls", which is actually confusing,
    unless you are in the __init__ method. Actually, you could
    use __init__ instead of __new__; it would work the same, but
    I feel that __init__ is somewhat simpler than __new__. Just
    my 0.02 cents
    
                             Michele
    
  5. 5. At 9:55 a.m. on 24 jul 2003, Stephan Diehl (the author) said:

    Excelent point. That's actually quite a nice point. ALL examples I've ever encountered ware doing the "magic" stuff within the __new__ method of the metaclass. Since a class is more or less an instance of its metaclass, it might be better to use __init__ instead of __new__ as __init__ is much more natural for most python programmers.

  6. 6. At 9:36 a.m. on 8 aug 2003, Michele Simionato said:

    __new__ vs __init__ in metaclasses. In this example __new__ and __init__ would be interchangeable, but in general overriding __init__ has severe limitations with respect to overriding __new__, since the 'name', 'bases' and 'dic' arguments cannot be directly changed. Let me show an example:

    from oopp import *
    
    class M(type):
        "Shows that dic cannot be modified in __init__, only in __new__"
        def __init__(cls,name,bases,dic):
            name='C name cannot be changed in __init__'
            bases='cannot be changed'
            dic['changed']=True
    
    class C(object):
        __metaclass__=M
        changed=False
    
    print C.__name__  # => C
    print C.__bases__ # => (,)
    print C.changed   # => False
    

    The output of this script is False: the dictionary cannot be changed in the __init__ method. However, replacing dic['changed']=True with cls.changed=True would work. Analougously, changing cls.__name__ would work. On the other hand, __bases__ is a read-only attribute and cannot be changed once the class has been created, therefore there is no way it can be touched in __init__. Nevertheless, __bases__ could be changed in __new__ before the class creation.

    These are the reasons why often people prefer __new__ over __init__.

               Michele
    
  7. 7. At 8:39 a.m. on 13 oct 2004, Ian Bicking said:

    Alternative descriptor implementation. I have an implementation that does something similar; it's actually a write-once implementation, since you have to set the value at least once. It doesn't use metaclasses, but instead uses a descriptor (similar to property):

    http://blog.colorstudy.com/ianb/weblog/2004/07/15.html#P131

Sign in to comment