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