ActiveState Code

Recipe 307969: Generating get/set methods using a metaclass


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

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
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


    

Discussion

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

Comments

  1. 1. At 8:44 a.m. on 13 oct 2004, Ian Bicking said:

    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')))
    
  2. 2. At 1:31 a.m. on 14 oct 2004, dav X said:

    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()
    

Sign in to comment