Welcome, guest | Sign In | My Account | Store | Cart

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, 33 lines
 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()

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

7 comments

Sean Ross 18 years, 6 months ago  # | flag

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

Sean Ross

Raymond Hettinger 18 years, 6 months ago  # | flag

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')
Sean Ross 18 years, 6 months ago  # | flag

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?

Michele Simionato 18 years, 4 months ago  # | flag

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
Stephan Diehl (author) 18 years, 4 months ago  # | flag

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.

Michele Simionato 18 years, 3 months ago  # | flag

__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
Ian Bicking 17 years, 1 month ago  # | flag

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

Created by Stephan Diehl on Sat, 3 May 2003 (PSF)
Python recipes (4591)
Stephan Diehl's recipes (5)

Required Modules

  • (none specified)

Other Information and Tasks