How to write a metaclass that will prevent the creation of new attributes after __init __ ()?

I am currently overriding the __setattr__ () class at the end of the __init__ () class method to prevent the creation of a new attribute -

 class Point(object): def __init__(self): self.x = 0 self.y = 0 Point.__setattr__ = self._setattr def _setattr(self, name, value): if not hasattr(self, name): raise AttributeError("'" + name + "' not an attribute of Point object.") else: super(Point, self).__setattr__(name, value) 

Is there a way to avoid manually overriding __setattr__ () and do it automatically using metaclasses?

The closest I have come -

 class attr_block_meta(type): def __new__(meta, cname, bases, dctry): def _setattr(self, name, value): if not hasattr(self, name): raise AttributeError("'" + name + "' not an attribute of " + cname + " object.") object.__setattr__(self, name, value) dctry.update({'x': 0, 'y': 0}) cls = type.__new__(meta, cname, bases, dctry) cls.__setattr__ = _setattr return cls class ImPoint(object): __metaclass__ = attr_block_meta 

Is there a more general way to do this so that a priori knowledge of the attributes of the subclass is not required?
Basically, how to avoid the dctry.update({'x': 0, 'y': 0}) line dctry.update({'x': 0, 'y': 0}) and make this work no matter what the class attribute names are?

PS - FWIW I already appreciated the parameters __slots__ and namedtuple and found that they are not enough for my needs. Please do not narrow your focus to the points discussed (), which I used to illustrate the issue; an actual use case includes a much more complex class.

+7
python metaclass
source share
4 answers

Will this make sense for your case?

 from functools import wraps class attr_block_meta(type): def __new__(meta, cname, bases, dctry): def _setattr(self, name, value): if not hasattr(self, name): raise AttributeError("'" + name + "' not an attibute of " + cname + " object.") object.__setattr__(self, name, value) def override_setattr_after(fn): @wraps(fn) def _wrapper(*args, **kwargs): cls.__setattr__ = object.__setattr__ fn(*args, **kwargs) cls.__setattr__ = _setattr return _wrapper cls = type.__new__(meta, cname, bases, dctry) cls.__init__ = override_setattr_after(cls.__init__) return cls class ImPoint(object): __metaclass__ = attr_block_meta def __init__(self, q, z): self.q = q self.z = z point = ImPoint(1, 2) print point.q, point.z point.w = 3 # Raises AttributeError 

For more on 'wraps' see.

You may have to play a little with it to make it more elegant, but the general idea is to override __setattr__ only after calling init .

Having said that, the general approach to this is to simply use object.__setattr__(self, field, value) internally to bypass AttributeError:

 class attr_block_meta(type): def __new__(meta, cname, bases, dctry): def _setattr(self, name, value): if not hasattr(self, name): raise AttributeError("'" + name + "' not an attibute of " + cname + " object.") object.__setattr__(self, name, value) cls = type.__new__(meta, cname, bases, dctry) cls.__setattr__ = _setattr return cls class ImPoint(object): __metaclass__ = attr_block_meta def __init__(self, q, z): object.__setattr__(self, 'q', q) object.__setattr__(self, 'z', z) point = ImPoint(1, 2) print point.q, point.z point.w = 3 # Raises AttributeError 
+6
source share

Do not reinvent the wheel.

Two simple ways to achieve this (without directly using the metaclass) are used:

For example, using namedtuple (based on an example in the docs):

 Point = namedtuple('Point', ['x', 'y']) p = Point(11, 22) pz = 33 # ERROR 

For example, using __slots__ :

 class Point(object): __slots__ = ['x', 'y'] def __init__(self, x=0, y=0): self.x = x self.y = y p = Point(11,22) pz = 33 # ERROR 
+10
source share

You do not need metaclasses to solve this problem.

If you want to create data earlier and then to be immutable, I would definitely use namedtuple , as shx2 suggests.

Otherwise, simply define the set of valid fields in the class and check __setattr__ to see if the name you are trying to set is in the collection of allowed fields. You do not need to change the implementation of the __setattr__ path through __init__ - it will work during __init__ just as it will work later. Use tuple or frozenset as the data structure for allowed fields if you want to refuse to change or change them in this class.

 class Point(object): _allowed_attrs = ("x", "y") def __init__(self, x, y): self.x = x self.y = y def __setattr__(self, name, value): if name not in self._allowed_attrs: raise AttributeError( "Cannot set attribute {!r} on type {}".format( name, self.__class__.__name__)) super(Point, self).__setattr__(name, value) p = Point(5, 10) px = 9 py = "some string" pz = 11 # raises AttributeError 

This can be easily taken into account in the base class for reuse:

 class RestrictedAttributesObject(object): _allowed_attrs = () def __setattr__(self, name, value): if name not in self._allowed_attrs: raise AttributeError( "Cannot set attribute {!r} on type {}".format( name, self.__class__.__name__)) super(RestrictedAttributesObject, self).__setattr__(name, value) class Point(RestrictedAttributesObject): _allowed_attrs = ("x", "y") def __init__(self, x, y): self.x = x self.y = y 

I don’t think it will be considered pythonic to block valid attributes of an object in this way, and this will cause some complexity for subclasses that need additional attributes (the subclass must ensure that the _allowed_attrs field has the corresponding contents).

+6
source share

I have the same need (for developing a quick-hack API). I do not use metaclasses for this, just inheritance:

 class LockedObject(object): def __setattr__(self, name, value): if name == "_locked": object.__setattr__(self, name, value) return if hasattr(self, "_locked"): if not self._locked or hasattr(self, name): object.__setattr__(self, name, value) else: raise NameError("Not allowed to create new attribute {} in locked object".format(name)) else: # never called _lock(), so go on object.__setattr__(self, name, value) def _lock(self): self._locked = True def _unlock(self): self._locked = False 

Then:

 class Base(LockedObject): def __init__(self): self.a = 0 self.b = 1 self._lock() 

If I need subclasses of Base and add additional attributes, I use unlock:

 class Child(Base): def __init__(self): Base.__init__(self) self._unlock() self.c = 2 self._lock() 

If the database is abstract, you can skip locking it and just block the children. I have some unittests that check that every open class is locked after init to catch me if I forget the lock.

+1
source share

All Articles