views:

214

answers:

6

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...)

+1  A: 

There 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.

Dan Udey
well, there is a way, it's just not as clean and concise as we'd want... -k
Kevin Little
You could set the attr. to property(), then NOTHING, not even the instance itself would be able to access it at all!
William Keller
(Although I'd also like to point out that your second point is valid!)
William Keller
Special extension module could make it much more 'difficult' to change selected attributes
J.F. Sebastian
+2  A: 
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...

Kevin Little
You can!@property will work, but you have to usedef readonly(self): return readonlyIt still avoids the lambda noise, though.
William Keller
This isn't a readonly attribute, because other code can still execute "c._C__readonly = 4"
John Millikin
Yes, I knew that was the "mangling" used for the "__" prefix. That's *really* cheating by the outside code, and if they're that desperate, I yield! :)
Kevin Little
+7  A: 

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
William Keller
Isn't this the equivalent to what I did, "readonly = property(lambda self: self.__readonly)" ?
Kevin Little
Yes, it is, you posted while I was fixing my markup.
William Keller
A: 

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
Ryan
+1  A: 

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.

Dan
A: 

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).

Ivan