Welcome, guest | Sign In | My Account | Store | Cart
NOTE: Recipes have moved! Please visit GitHub.com/activestate/code for the current versions.

This is an example of metaclass-as-code-generator. The metaclass writes the requested get/set methods automatically.

Python, 75 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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
#!/usr/bin/python

# Helpers

def _addMethod(fldName, clsName, verb, methodMaker, dict):
    """Make a get or set method and add it to dict."""
    compiledName = _getCompiledName(fldName, clsName)
    methodName = _getMethodName(fldName, verb)
    dict[methodName] = methodMaker(compiledName)
    
def _getCompiledName(fldName, clsName):
    """Return mangled fldName if necessary, else no change."""
    # If fldName starts with 2 underscores and does *not* end with 2 underscores...
    if fldName[:2] == '__' and fldName[-2:] != '__':
        return "_%s%s" % (clsName, fldName)
    else:
        return fldName

def _getMethodName(fldName, verb):
    """'_salary', 'get'  => 'getSalary'"""
    s = fldName.lstrip('_') # Remove leading underscores
    return verb + s.capitalize()

def _makeGetter(compiledName):
    """Return a method that gets compiledName's value."""
    return lambda self: self.__dict__[compiledName]

def _makeSetter(compiledName):
    """Return a method that sets compiledName's value."""    
    return lambda self, value: setattr(self, compiledName, value)

class Accessors(type):
    """Adds accessor methods to a class."""
    def __new__(cls, clsName, bases, dict):
        for fldName in dict.get('_READ', []) + dict.get('_READ_WRITE', []):
            _addMethod(fldName, clsName, 'get', _makeGetter, dict)
        for fldName in dict.get('_WRITE', []) + dict.get('_READ_WRITE', []):
            _addMethod(fldName, clsName, 'set', _makeSetter, dict)
        return type.__new__(cls, clsName, bases, dict)

if __name__ == "__main__":
    
    class Employee:
        __metaclass__ = Accessors
        _READ_WRITE = ['name', 'salary', 'title', 'bonus']
        def __init__(self, name, salary, title, bonus=0):
            self.name = name
            self.salary = salary
            self.title = title
            self.bonus = bonus
    b = Employee('Joe Test', 40000, 'Developer')
    print 'Name:', b.getName()
    print 'Salary:', b.getSalary()
    print 'Title:', b.getTitle()
    print 'Bonus:', b.getBonus()
    b.setBonus(5000)
    print 'Bonus:', b.getBonus()

    class ReadOnly:
        __metaclass__ = Accessors
        _READ = ['__data']
        def __init__(self, data):
            self.__data = data
    ro = ReadOnly('test12345')
    print 'Read-only data:', ro.getData()

    class WriteOnly:
        __metaclass__ = Accessors
        _WRITE = ['_data']
        def __init__(self, data):
            self._data = data
    wo = WriteOnly('test67890')
    print 'Write-only data:', wo._data    
    wo.setData('xzy123')
    print 'Write-only data:', wo._data


    

Metaclasses provide some of the code-generation power of Lisp macros. You can change the language without having to hack the compiler.

This recipe handles all the leading-underscore name variations correctly.

A caveat: The metaclass doesn't do any creation/initialization of the actual data attributes. You should handle that as you normally would, i.e. in an __init__ method.

Related recipes:

Generating get/set methods using closures: http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/259111

Simple read only attributes with meta-class programming: http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/197965

2 comments

Ian Bicking 12 years, 11 months ago  # | flag

using naming conventions. This is the implementation of a similar idea that I use in SQLObject ( http://sqlobject.org ). This function simply looks for appropriately-named methods, and creates properties based on that.

While it's just a builder function, that function can be used in a metaclass fairly easily to automatically call the function with the new class as an argument. To make a read-only attribute, you simply don't create a _set_attr method for that attribute.

def makeProperties(obj):
    """
    This function takes a dictionary of methods and finds
    methods named like:
    * _get_attr
    * _set_attr
    * _del_attr
    * _doc_attr
    Except for _doc_attr, these should be methods.  It
    then creates properties from these methods, like
    property(_get_attr, _set_attr, _del_attr, _doc_attr).
    Missing methods are okay.
    """

    if isinstance(obj, dict):
        def setFunc(var, value):
            obj[var] = value
        d = obj
    else:
        def setFunc(var, value):
            setattr(obj, var, value)
        d = obj.__dict__

    props = {}
    for var, value in d.items():
        if var.startswith('_set_'):
            props.setdefault(var[5:], {})['set'] = value
        elif var.startswith('_get_'):
            props.setdefault(var[5:], {})['get'] = value
        elif var.startswith('_del_'):
            props.setdefault(var[5:], {})['del'] = value
        elif var.startswith('_doc_'):
            props.setdefault(var[5:], {})['doc'] = value
    for var, setters in props.items():
        if len(setters) == 1 and setters.has_key('doc'):
            continue
        if d.has_key(var):
            if isinstance(d[var], types.MethodType) \
                   or isinstance(d[var], types.FunctionType):
                warnings.warn(
                    "I tried to set the property %r, but it was "
                    "already set, as a method (%r).  Methods have "
                    "significantly different semantics than properties, "
                    "and this may be a sign of a bug in your code."
                    % (var, d[var]))
            continue
        setFunc(var,
                property(setters.get('get'), setters.get('set'),
                         setters.get('del'), setters.get('doc')))
dav X 12 years, 11 months ago  # | flag

overloading accessors... In this example you cannot overload the generated accessors. You ca do this by adding a line to the _addMethod function:

def _addMethod(fldName, clsName, verb, methodMaker, dict):
    """Make a get or set method and add it to dict."""
    compiledName = _getCompiledName(fldName, clsName)
    methodName = _getMethodName(fldName, verb)
    if not dict.has_key(methodName):
        dict[methodName] = methodMaker(compiledName)

then:

if __name__ == "__main__":
    class Employee:
        __metaclass__ = Accessors
        _READ_WRITE = ['name']
        def __init__(self, name):
            self.name = name

        def getName(self):
            return 'my name is: %s' % self.name

    e = Employee('Machin chose')
    print e.getName()
Created by Robert Follek on Mon, 11 Oct 2004 (PSF)
Python recipes (4591)
Robert Follek's recipes (1)

Required Modules

  • (none specified)

Other Information and Tasks