Search code examples
pythonpropertiesclosuresmayapymel

How should I create properties using a closure in python?


I'm doing some coding for Maya with PyMel and I'm trying to create some properties in my rig class to wrap some PyMel code. The code for all of the properties was pretty similar, so I figured this would be a good place to use a closure.

import pymel.core as pm
import riggermortis.utils as utils

class RigModule(object):
    def __init__:
        # blah blah irrelevant code here
        pass

    def createRootProperty(attrName):
        def getter(self):
            return pm.getAttr(self.root+'.'+attrName)
        def setter(self, nodeToLink):
            if self.root.hasAttr(attrName):
                pm.setAttr(self.root+'.'+attrName, nodeToLink)
            else:
                utils.linkNodes(self.root, nodeToLink, attrName)
        return property(fget=getter,fset=setter)

    hookConstraint = createRootProperty('hookConstraint')
    unhookTarget = createRootProperty('unhookTarget')
    moduleGrp = createRootProperty('moduleGrp')
    hookGrp = createRootProperty('hookGrp')

Functionally it works, but Eclipse/PyDev is telling me that my 'createRootProperty' function needs 'self' as its first argument so I'm wondering if what I'm doing is incorrect.


Solution

  • For what you're doing a closure is not really needed except for cleanliness. The linter think it's an incorrectly formatted member function, even though it's doing what you want.

    You can just move the function of the class scope and the linter will stop complaining -- you can rename the function with an underscore so nobody accidentally thinks it is a tool rather than a piece of infrastructure.

    If you expect to do this a lot, you could automate it into a metaclass that reads a list of names from a class field and creates properies as appropriate. There's a more detailed example of that strategy here, but in essence the metaclass will get a copy of the class dictionary when the class is defined and it has the opportunity to mess with the definition before it gets compiled. You can create a property easily at that step:

    def createRootProperty(name):
        # this is a dummy, but as long as
        # the return from this function
        # is a property descriptor you're good
        @property
        def me(self):
            return name, self.__class__
    
        return me
    
    
    class PropertyMeta(type):
    
        # this gets called when a class using this meta is
        # first compiled.  It gives you a chance to intervene
        # in the class creation project
    
        def __new__(cls, name, bases, properties):
            # if the class has a 'PROPS' member, it's a list 
            # of properties to add
            roots = properties.get('PROPS', [])
            for r in roots:
                properties[r] = createRootProperty(r)
                print ("# added property '{}' to {}".format(r, name))
    
            return type.__new__( cls, name, bases, properties)
    
    
    class RigModule(object):
        __metaclass__ = PropertyMeta
        PROPS = ['arm', 'head', 'leg']
    
        def __init__(self):
            pass
    
    test = RigModule()
    print test.arm
    
    class Submodule(RigModule):
        # metaclass added properties are inheritable
        pass
    
    
    test2 = Submodule()
    print test2.leg
    
    class NewProperties(RigModule):
        # they can also be augmented in derived classes
        PROPS = ['nose', 'mouth']
    
    print NewProperties().nose
    print NewProperties().arm
    
    
    # added property 'arm' to RigModule
    # added property 'head' to RigModule
    # added property 'leg' to RigModule
    #  ('arm', <class '__main__.RigModule'>)
    #  ('leg', <class '__main__.Submodule'>)
    # added property 'nose' to NewProperties
    # added property 'mouth' to NewProperties
    # ('nose', <class '__main__.NewProperties'>)
    # ('arm', <class '__main__.NewProperties'>)
    

    Metaclasses get a bad rep -- sometimes deservedly so -- for adding complexity. Don't use them when a simpler approach will do. But for boilerplate reduction in cases like this they are a great tool.