Recipe 6.3. Restricting Attribute Setting
Credit: Michele Simionato
Problem
Python normally lets you
freely add attributes to classes and their instances. However, you
want to restrict that freedom for some class.
Solution
Special method _ _setattr_
_ intercepts every setting of an attribute, so it lets you
inhibit the addition of new attributes that were not already present.
One elegant way to implement this idea is to code a class, a simple
custom metaclass, and a wrapper function, all cooperating for the
purpose, as follows:
def no_new_attributes(wrapped_setattr):
"" raise an error on attempts to add a new attribute, while
allowing existing attributes to be set to new values.
""
def _ _setattr_ _(self, name, value):
if hasattr(self, name): # not a new attribute, allow setting
wrapped_setattr(self, name, value)
else: # a new attribute, forbid adding it
raise AttributeError("can't add attribute %r to %s" % (name, self))
return _ _setattr_ _
class NoNewAttrs(object):
"" subclasses of NoNewAttrs inhibit addition of new attributes, while
allowing existing attributed to be set to new values.
""
# block the addition new attributes to instances of this class
_ _setattr_ _ = no_new_attributes(object._ _setattr_ _)
class _ _metaclass_ _(type):
" simple custom metaclass to block adding new attributes to this class "
_ _setattr_ _ = no_new_attributes(type._ _setattr_ _)
Discussion
For
various reasons, you sometimes want to restrict
Python's dynamism. In particular, you may want to
get an exception when a new attribute is accidentally set on a
certain class or one of its instances. This recipe shows how to go
about implementing such a restriction. The key point of the recipe
is, don't use _
_slots_ _ for this purpose: _ _slots_ _
is intended for a completely different task (i.e., saving memory by
avoiding each instance having a dictionary, as it normally would,
when you need to have vast numbers of instances of a class with just
a few fixed attributes). _ _slots_ _ performs its
intended task well but has various limitations when you try to
stretch it to perform, instead, the task this recipe covers. (See
Recipe 6.18 for an example
of the appropriate use of _ _slots_ _ to save
memory.)Notice that this recipe inhibits the addition of runtime attributes,
not only to class instances, but also to the class itself, thanks to
the simple custom metaclass it defines. When you want to inhibit
accidental addition of attributes, you usually want to inhibit it on
the class as well as on each individual instance. On the other hand,
existing attributes on both the class and its instances may be freely
set to new values.Here is an example of how you could use this recipe:
class Person(NoNewAttrs):The point of inheriting from NoNewAttrs is forcing
firstname = ''
lastname = ''
def _ _init_ _(self, firstname, lastname):
self.firstname = firstname
self.lastname = lastname
def _ _repr_ _(self):
return 'Person(%r, %r)' % (self.firstname, self.lastname)
me = Person("Michere", "Simionato")
print me
# emits: Person('Michere', 'Simionato')
# oops, wrong value for firstname, can we fix it? Sure, no problem!
me.firstname = "Michele"
print me
# emits: Person('Michele', 'Simionato')
yourself to "declare" all allowed
attributes by setting them at class level in the body of the class
itself. Any further attempt to set a new,
"undeclared" attribute raises an
AttributeError:
try: Person.address = ''In some ways, therefore, subclasses of NoNewAttr and
except AttributeError, err: print 'raised %r as expected' % err
try: me.address = ''
except AttributeError, err: print 'raised %r as expected' % err
their instances behave more like Java or C++ classes and instances,
rather than normal Python ones. Thus, one use case for this recipe is
when you're coding in Python a prototype that you
already know will eventually have to be recoded in a less dynamic
language.
See Also
Library Reference and Python in a
Nutshell documentation on the special method _
_setattr_ _ and on custom metaclasses; Recipe 6.18 for an example of an
appropriate use of _ _slots_ _ to save memory;
Recipe 6.2 for a class
that is the complement of this one.