Reputation: 6819
I'm trying to get into metaclass programming in Python and I'd like to know how to restrict attribute type with metaclass. It's quite easy with the descriptors, but what about metaclasses?
Here is short example:
>>> class Image(Object):
... height = 0
... width = 0
... path = '/tmp'
... size = 0
>>> img = Image()
>>> img.height = 340
>>> img.height
340
>>> img.path = '/tmp/x00.jpeg'
>>> img.path
'/tmp/x00.jpeg'
>>> img.path = 320
Traceback (most recent call last):
...
TypeError
Python version is 2.7
Upvotes: 4
Views: 1153
Reputation: 9104
Just override __setattr__
in the metaclass and check default type for every attribute during initialization:
>>> class Meta(type):
def __new__(meta, name, bases, dict):
def _check(self, attr, value):
if attr in self.defaults:
if not isinstance(value, self.defaults[attr]):
raise TypeError('%s cannot be %s' % (attr, type(value)))
else:
self.defaults[attr] = type(value)
def _setattr(self, attr, value):
_check(self, attr, value)
object.__setattr__(self, attr, value)
cls = type.__new__(meta, name, bases, dict)
# Set up default type for every attribute
cls.defaults = {name: type(value) for name, value in dict.items()}
cls.__setattr__ = _setattr
return cls
>>> class Image(object):
__metaclass__ = Meta
height = 0
width = 0
path = '/tmp'
size = 0
>>> i = Image()
>>> i.height = 240
>>> i.height
240
>>> i.size
0
>>> i.size = 7
>>> i.size
7
>>> i.path = '/tmp/subdir'
>>> i.path
'/tmp/subdir'
>>> i.path = 23
TypeError: path cannot be <type 'int'>
Alternative (and maybe more elegant) method:
class MetaBase(object):
def _check(self, attr, value):
if attr in self.defaults:
if not isinstance(value, self.defaults[attr]):
raise TypeError('%s cannot be %s' % (attr, type(value)))
else:
self.defaults[attr] = type(value)
def __setattr__(self, attr, value):
self._check(attr, value)
super(MetaBase, self).__setattr__(attr, value)
class Meta(type):
def __new__(meta, name, bases, dict):
cls = type.__new__(meta, name, (MetaBase,) + bases, dict)
cls.defaults = {name: type(value) for name, value in dict.items()}
return cls
class Image(object):
__metaclass__ = Meta
height = 0
width = 0
path = '/tmp'
size = 0
Behaviour is the same as before
Upvotes: 8
Reputation: 287865
Overwrite __setattr__
in the metaclass. Note that you'll have to check the initial values (height=0, path = '/tmp'
in your example) separately:
class RestrictAttrs(type):
def __new__(mcs, name, bases, dct):
def _checkattr(k, v):
if k == 'path':
if not isinstance(v, str):
raise TypeError('path must be a str!')
def _setattr(self, k, v):
_checkattr(k, v)
self.__dict__[k] = v
# Check of initial values (optional)
for k,v in dct.items():
_checkattr(k, v)
res = type.__new__(mcs, name, bases, dct)
res.__setattr__ = _setattr
return res
class Image(object):
__metaclass__ = RestrictAttrs
path = '/tmp'
i = Image()
i.path = 32
Upvotes: 3