How to get MRO in __new__() of metaclasses?

Frank von Delft fvondelft at syrrx.com
Mon Dec 9 15:39:33 EST 2002


> Explaining what you're trying to acheive may (no promises :) help us
> give better answers.

Sure, I here's the metaclass, with little illustrations;  check out
the docstrings.  The bit I struggle with is marked with CAPS.

--------------------------------------------------------------------------------
class MetaEnforceProperties(type):
    '''Metaclass to ensure that derived classes do not overwrite
    properties from the base class in an attempt to assign values
    to them.  Therefore, it restores the property definition to
    the derived class, and moves the assignment of value into
    __init__() of the derived class, so that the value assignment
    still occurs via the property fset function.'''
    
    def __new__(cls,name,bases,dict):

        # Generate the list of classes for MRO.  
        # THIS IS NOT THE RIGHT __MRO__!!!!!  It's the classic mro
        # -- one day I'll grow up and know how to get the new-style
        # MRO.)
        def getMRO(klass):
            '''Make provision for classic classes, which don't have
            the __mro__ attribute.'''
            try: return klass.__mro__
            except AttributeError: return ()
        mroBasesList = reduce(lambda x,y: x+y, [
                                  getMRO(base) for base in bases ])

        # Search all base classes to see if any attributes of the new
        # class has already been defined as property (ppty);  if it
        # has, restore the original (base) property definition to the
        # new class, but remember its value, so that it can be
        # assigned at object instantiation. We need only look as far
        # as the first occurrence of the attribute in the MRO
        # sequence; this is sufficient to carry property definitions
        # down the inheritance tree:  if the property of a super-
        # superclass was reassigned in a superclass, it will have
        # been restored to that (most recent) superclass by the time
        # the current class is created.  
        pptyValues = {}
        for attr in dict:
            for klass in mroBasesList:
                if attr in klass.__dict__:
                    if isinstance(klass.__dict__[attr],property):
                        pptyValues[attr] = dict[attr]
                        dict[attr] = klass.__dict__[attr]
                        print 'Restoring %s.%s = %s as property' %(
                                           name,attr,pptyValues[attr])
                    break
                        
        # Gather together ppty values from all base classes into one
        # class variable, _pptyValues.  This is necessary because
        # values are only assigned when [base] classes are
        # instantiated; until then, they languish in the _pptyValues
        # class variable, so we have to retrieve them.
        allPptyValues = {}
        list(mroBasesList).reverse()    # Most recent assignments win
        for klass in mroBasesList:
            allPptyValues.update(klass.__dict__.get('_pptyValues',{}))
        allPptyValues.update(pptyValues)
        dict.setdefault('_pptyValues',{}).update(allPptyValues)
        
        # Now make a new __init__(), which will assign the stored
        # values to the restored properties upon instantiation,
        # allowing the properties to do their checks.  However,
        # must also retainthe explicit __init__(), if it exists;
        # it goes last.
        oldInit = dict.get('__init__',lambda *a,**kw: None)
        def newInit(self,*args,**kwargs):
            for k,v in self._pptyValues.iteritems():
                setattr(self,k,v); 
            oldInit(self,*args,**kwargs)
        dict['__init__'] = newInit

        # Let There Be Class.
        return type.__new__(cls,name,bases,dict)


#==================================================================

class Base(object):
    '''Contains a few properties that enforce a restricted range
    set of values on attributes. '''

    __metaclass__ = MetaEnforceProperties

    _ammos = ['sheep','goose','laVache','woodenRabbit']
    def _setAmmo(self,ammo):
        if ammo in Base._ammos:
            self._ammo = ammo
        else:
            raise ValueError, '%s not in %s' %(ammo,Base._ammos)
    def _getAmmo(self):
        return self._ammo
    ammo = property(_getAmmo,_setAmmo,None,
            '''Property to restrict catapult ammunition.''')
    

    _perils = ['bunny','maidensOfAnthrax','beastOfAaaargh','French']
    def _setPeril(self,peril):
        if peril in Base._perils:
            self._peril = peril
        else:
            raise ValueError, '%s not in %s'    %(peril,Base._perils)
    def _getPeril(self):
        return self._peril
    peril = property(_getPeril,_setPeril,None,
                '''Property to restrict Sir Galahad's doom''')


#==================================================================
        
class Preparation(Base):
    ammo = 'sheep'

class Quest(Preparation):
    peril = 'French'

class BadPrep(Preparation):
    def __init__(self):
        print 'BadPrep.__init__(), ammo before and after:', self.ammo,
        self.ammo = 'holyHandGranade'
        #self.ammo = 'goose'
        print self.ammo

class BadQuest(Preparation,Quest):
    peril = 'bridgeOfDeath'


#==================================================================
        
if __name__ == '__main__':
    p1 = Preparation()
    print 'p1 ammo:',p1.ammo
    q1 = Quest()
    print 'q1 ammo, peril:',q1.ammo,q1.peril

    try: p2 = BadPrep()
    except ValueError, e: print 'ERROR',e
    try: q2 = BadQuest()
    except ValueError, e: print 'ERROR',e
    

    
--------------------------------------------------------------------------------
Some output:
============
>>> 
Restoring Preparation.ammo = sheep as property
Restoring Quest.peril = French as property
Restoring BadQuest.peril = bridgeOfDeath as property
p1 ammo: sheep
q1 ammo, peril: sheep French
BadPrep.__init__(), ammo before and after: sheep ERROR holyHandGranade
not in ['sheep', 'goose', 'laVache', 'woodenRabbit']
ERROR bridgeOfDeath not in ['bunny', 'maidensOfAnthrax',
'beastOfAaaargh', 'French']

--------------------------------------------------------------------------------

I'm trying to provide base classes that don't allow derived classes
too much freedom;  in particular, the base class provides methods and
restrictions (i.e. propeties), while the derived classes does the
configuration (i.e. setting of values).
      Base (restricts)  --->  Deriv (configures)  --->  Object (uses)

I strongly suspect there may be a better way of doing this.  e.g.
doing the configuration in Deriv.__init__() instead of with class
variables, as in BadPrep (although I do want the configuration in
Deriv to be automatically inherited);  or maybe with intelligent
exceptions?  (Other suggestions?)   But I rather like properties, and
at least I now grok metaclasses :-)

Cheers
Phraenquex



More information about the Python-list mailing list