Restricted attribute writing

John O'Hagan research at johnohagan.com
Mon Aug 8 00:59:23 EDT 2011


On Mon, 08 Aug 2011 03:07:30 +1000
Steven D'Aprano <steve+comp.lang.python at pearwood.info> wrote:

> John O'Hagan wrote:
> 
> > I'm looking for good ways to ensure that attributes are only writable such
> > that they retain the characteristics the class requires.
> 
> That's what properties are for.
> 
> > My particular case is a class attribute which is initialised as a list of
> > lists of two integers, the first of which is a modulo remainder. I need to
> > be able to write to it like a normal list, but want to ensure it is only
> > possible to do so without changing that format.
> 
> Then you have two problems to solve.
> 
> First, you need a special type of list that only holds exactly two integers.
> Your main class can't control what happens inside the list, so you need the
> list to validate itself.
> 
> Secondly, you should use a property in your main class to ensure that the
> attribute you want to be a special list-of-two-ints can't (easily) be
> changed to something else.
>

Although experience shows you're usually right :) , I thought I had three problems, the third being what I perhaps wasn't clear enough about: that the two-integer containers live in a list which should only contain the two-integer things, but aside from that should be able to do all the other list operations on it. AFAIK making this attribute a property only protects it from incorrect assignment, but not from unwanted appends etc.  

That's what the other helper class Order is meant for, it subclasses list, and overrides __setitem__ to ensure every item is an OrderElement, and __getitem__ to ensure slices are the same class. I've also since realised it must override append, insert and extend. I think I need all this to ensure the required behaviour, including:

s = SeqSim([[15, 2]], 12)
s.order[0][1] = 100
s.order[0][1:] = [100]
s.order += [[22, 11]]
s.order *= 2
s.order[2] = [[15, 8]]
s.order[1:5:2]) = [[1, 1],[2, 2]]
s.order.extend([[1, 1],[2, 2]])
s.order.insert(2, [2, 29])
s.order.append([26, 24])
s.order.extend(s.order[1:3])
s.order = [[99, 99],[100, 100]]
import random
random.shuffle(s.order)
etc
[...]
> I'd take this approach instead:
> 
> # Untested.
> class ThingWithTwoIntegers(object):
>     def __init__(self, a, b):
>         self.a = a
>         self.b = b
>     def __getitem__(self, index):
>         # Slicing not supported, because I'm lazy.
>         if index < 0: index += 2
>         if index == 0: return self.a
>         elif index == 1: return self.b
>         else: raise IndexError
>     def __setitem__(self, index, value):
>         # Slicing not supported, because I'm lazy.
>         if index < 0: index += 2
>         if index == 0: self.a = value
>         elif index == 1: self.b = value
>         else: raise IndexError
>     def _geta(self):
>         return self._a
>     def _seta(self, value):
>         if isinstance(value, (int, long)):  # drop long if using Python 3
>             self._a = value
>         else:
>             raise TypeError('expected an int but got %s' % type(value))
>     a = property(_geta, _seta)
>     # and the same for b: _getb, _setb, making the obvious changes
> 
[...]
> Obviously this isn't a full blown list, but if you don't need all the
> list-like behaviour (sorting, inserting, deleting items, etc.) why support
> it?
> 

Thanks for this, I can see that the __data attribute I was using was unnecessary and I've redone the OrderElement class accordingly, although I do want slicing and don't need dot-notation access:

class OrderElement():

    def __init__(self, length, a, b):
        self.__length=length
        self.__a = a
        self.__b = b
        self[:] = a, b
        
    def __setitem__(self, index, item):
        if isinstance(index, slice):
            for k, i in zip(range(*index.indices(2)), item):
                self[k] = i
        elif isinstance(item, int) and index in (0, 1):
            if index == 0:
                self.__a = item % self.__length
            elif index == 1:
                self.__b = item
        else:
            raise TypeError("OrderElement takes two integers")
            
    def __getitem__(self, index):
        if isinstance(index, slice):
           return [self[i] for i in range(*index.indices(2))]
        if index == 0:
            return self.__a
        if index == 1:
            return self.__b
        raise IndexError

As for the rest, I take your point that a simple idea need not be simple to implement, and I'm starting to think my solution may be about as complicated as it needs to be. 

Regards,

John



More information about the Python-list mailing list