cval
cval

Reputation: 6819

Restricting attribute type with metaclass

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

Answers (2)

rubik
rubik

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

phihag
phihag

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

Related Questions