In Python, I want to make selected instance attributes of a class be readonly to code outside of the class. I want there to be no way outside code can alter the attribute, except indirectly by invoking methods on the instance. I want the syntax to be concise. What is the best way? (I give my current best answer below...)
views:
214answers:
6There is no real way to do this. There are ways to make it more 'difficult', but there's no concept of completely hidden, inaccessible class attributes.
If the person using your class can't be trusted to follow the API docs, then that's their own problem. Protecting people from doing stupid stuff just means that they will do far more elaborate, complicated, and damaging stupid stuff to try to do whatever they shouldn't have been doing in the first place.
class C(object):
def __init__(self):
self.fullaccess = 0
self.__readonly = 22 # almost invisible to outside code...
# define a publicly visible, read-only version of '__readonly':
readonly = property(lambda self: self.__readonly)
def inc_readonly( self ):
self.__readonly += 1
c=C()
# prove regular attribute is RW...
print "c.fullaccess = %s" % c.fullaccess
c.fullaccess = 1234
print "c.fullaccess = %s" % c.fullaccess
# prove 'readonly' is a read-only attribute
print "c.readonly = %s" % c.readonly
try:
c.readonly = 3
except AttributeError:
print "Can't change c.readonly"
print "c.readonly = %s" % c.readonly
# change 'readonly' indirectly...
c.inc_readonly()
print "c.readonly = %s" % c.readonly
This outputs:
$ python ./p.py
c.fullaccess = 0
c.fullaccess = 1234
c.readonly = 22
Can't change c.readonly
c.readonly = 22
c.readonly = 23
My fingers itch to be able to say
@readonly
self.readonly = 22
i.e., use a decorator on an attribute. It would be so clean...
You should use the @property
decorator.
>>> class a(object):
... def __init__(self, x):
... self.x = x
... @property
... def xval(self):
... return self.x
...
>>> b = a(5)
>>> b.xval
5
>>> b.xval = 6
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: can't set attribute
You could use a metaclass that auto-wraps methods (or class attributes) that follow a naming convention into properties (shamelessly taken from Unifying Types and Classes in Python 2.2:
class autoprop(type):
def __init__(cls, name, bases, dict):
super(autoprop, cls).__init__(name, bases, dict)
props = {}
for name in dict.keys():
if name.startswith("_get_") or name.startswith("_set_"):
props[name[5:]] = 1
for name in props.keys():
fget = getattr(cls, "_get_%s" % name, None)
fset = getattr(cls, "_set_%s" % name, None)
setattr(cls, name, property(fget, fset))
This allows you to use:
class A:
__metaclass__ = autosuprop
def _readonly(self):
return __x
Here's how:
class whatever(object):
def __init__(self, a, b, c, ...):
self.__foobar = 1
self.__blahblah = 2
foobar = property(lambda self: self.__foobar)
blahblah = property(lambda self: self.__blahblah)
(Assuming foobar
and blahblah
are the attributes you want to be read-only.) Prepending two underscores to an attribute name effectively hides it from outside the class, so the internal versions won't be accessible from the outside. This only works for new-style classes inheriting from object since it depends on property
.
On the other hand... this is a pretty silly thing to do. Keeping variables private seems to be an obsession that comes from C++ and Java. Your users should use the public interface to your class because it's well-designed, not because you force them to.
Edit: Looks like Kevin already posted a similar version.
I am aware that William Keller is the cleanest solution by far.. but here's something I came up with..
class readonly(object):
def __init__(self, attribute_name):
self.attribute_name = attribute_name
def __get__(self, instance, instance_type):
if instance != None:
return getattr(instance, self.attribute_name)
else:
raise AttributeError("class %s has no attribute %s" %
(instance_type.__name__, self.attribute_name))
def __set__(self, instance, value):
raise AttributeError("attribute %s is readonly" %
self.attribute_name)
And here's the usage example
class a(object):
def __init__(self, x):
self.x = x
xval = readonly("x")
Unfortunately this solution can't handle private variables (__ named variables).