4

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

2
  • I assume this is for education only and you don't intent to actually bolt static type checking onto Python. Commented Apr 30, 2012 at 11:53
  • 1
    Do you mean to use the metaclass to set up the descriptors? Commented Apr 30, 2012 at 12:04

2 Answers 2

8

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

Sign up to request clarification or add additional context in comments.

Comments

3

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

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.