Restricted attribute writing

J

John O'Hagan

I'm looking for good ways to ensure that attributes are only writable such that they retain the characteristics the class requires.

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.

Below is a what I eventually came up with; a container class called OrderElement for the inner lists, and subclass of list called Order for the main attribute, which is a property of the main class, simplified below as SeqSim. It works, but seems like a hell of a lot of code for a simple idea. I'm interested in ideas for simpler solutions, and general advice on how to do this kind of thing in a straightforward way.


class OrderElement():
"""Container class which can only hold two integers
the first of which is a modulo of the 'length' arg"""
def __init__(self, lis, length):
self.__data = [None, None]
self.__length=length
self[:] = lis

def __setitem__(self, index, item):
if isinstance(index, slice):
inds = range(*index.indices(2))
for k, v in enumerate(item):
self[inds[k]] = v
elif isinstance(item, int):
if index == 0:
item %= self.__length
self.__data[index] = item
else:
raise TypeError("OrderElement takes two integers")

def __getitem__(self, index):
return self.__data[index]


class Order(list):
"""Can only contain OrderElements"""
def __init__(self, lis, length):
self.__length = length
self[:] = lis

def __setitem__(self, index, item):
if isinstance(index, slice):
item = [i if isinstance(i, OrderElement)
else OrderElement(i, self.__length)
for i in item]
elif not isinstance(item, OrderElement):
item = OrderElement(item, self.__length)
list.__setitem__(self, index, item)

def __getitem__(self, index):
"""Ensure slices are of the same class"""
if isinstance(index, slice):
return self.__class__(list.__getitem__(self, index),
self.__length)
return list.__getitem__(self, index)


class SeqSim():
"""Just the relevant bits of the main class"""
def __init__(self, lis, length):
self.__order = Order(lis, length)
self.length = length

@property
def order(self):
return self.__order

@order.setter
def order(self, lis):
if not isinstance(lis, Order):
lis = Order(lis, self.length)
self.__order = lis
 
R

Roy Smith

John O'Hagan said:
I'm looking for good ways to ensure that attributes are only writable such
that they retain the characteristics the class requires.

Sounds like you're trying to do
http://en.wikipedia.org/wiki/Design_by_contract. Which is not a bad
thing. But, I think a more pythonic way to implement this would be to
verify behaviors, not types.

I would start by writing a assert_invarient() method which validates the
object. I'm guessing all you really need is that you can index [0] and
[1] and get ints, so test for that. Something like:

def assert_invarient(self):
try:
assert isinstance(data[0], int)
assert isinstance(data[1], int)
except:
raise ValueError

Then, call this from inside your __init__(), __setitem__(), etc.
 
S

Steven D'Aprano

Roy said:
John O'Hagan said:
I'm looking for good ways to ensure that attributes are only writable
such that they retain the characteristics the class requires.

Sounds like you're trying to do
http://en.wikipedia.org/wiki/Design_by_contract. Which is not a bad
thing. But, I think a more pythonic way to implement this would be to
verify behaviors, not types.

I would start by writing a assert_invarient() method which validates the
object. I'm guessing all you really need is that you can index [0] and
[1] and get ints, so test for that. Something like:

def assert_invarient(self):
try:
assert isinstance(data[0], int)
assert isinstance(data[1], int)
except:
raise ValueError

Don't do that. assert is for testing program logic, not verifying data. The
problem with assert is that the user can turn all assertions off, simply by
launching Python with the -O switch. Your verification code then becomes:

def assert_invarient(self):
try:
pass
except:
raise ValueError

which is useless.

When should you use an assertion? If you've ever written code like this:

if condition:
do_something()
else:
# This should never happen. But you know what they say: code that
# can't happen, does!
raise RuntimeError('condition unexpectedly false')


that's a prime candidate for turning into an assertion:


assert condition, 'condition unexpectedly false'
do_something()
 
R

Rafael Durán Castañeda

I think you might use a tuple instead of a list for OrderElement, that would
make much easier your code:

class
OrderElement(tuple):

def __new__(cls, x, y):
if not isinstance(x, int) or not isinstance(y, int):
raise TypeError("Order element must receives two
integers")

return tuple.__new__(cls, (x, y))


class Order(list):
def __setitem__(self, item):
assert isinstance(item, OrderElement)
super(Order, self).__setitem__(item)


I didn't check your module condition since it isn't quite clear to me, but
you could add a second condition two Order class.

2011/8/7 Steven D'Aprano said:
Roy said:
John O'Hagan said:
I'm looking for good ways to ensure that attributes are only writable
such that they retain the characteristics the class requires.

Sounds like you're trying to do
http://en.wikipedia.org/wiki/Design_by_contract. Which is not a bad
thing. But, I think a more pythonic way to implement this would be to
verify behaviors, not types.

I would start by writing a assert_invarient() method which validates the
object. I'm guessing all you really need is that you can index [0] and
[1] and get ints, so test for that. Something like:

def assert_invarient(self):
try:
assert isinstance(data[0], int)
assert isinstance(data[1], int)
except:
raise ValueError

Don't do that. assert is for testing program logic, not verifying data. The
problem with assert is that the user can turn all assertions off, simply by
launching Python with the -O switch. Your verification code then becomes:

def assert_invarient(self):
try:
pass
except:
raise ValueError

which is useless.

When should you use an assertion? If you've ever written code like this:

if condition:
do_something()
else:
# This should never happen. But you know what they say: code that
# can't happen, does!
raise RuntimeError('condition unexpectedly false')


that's a prime candidate for turning into an assertion:


assert condition, 'condition unexpectedly false'
do_something()
 
R

Rafael Durán Castañeda

The assert on Order should be an if ... raise, like OrderElement, sorry for
the mistake and repost

El 7 de agosto de 2011 18:53, Rafael Durán Castañeda <
(e-mail address removed)> escribió:
I think you might use a tuple instead of a list for OrderElement, that
would make much easier your code:

class
OrderElement(tuple):

def __new__(cls, x, y):
if not isinstance(x, int) or not isinstance(y, int):
raise TypeError("Order element must receives two
integers")

return tuple.__new__(cls, (x, y))


class Order(list):
def __setitem__(self, item):
assert isinstance(item, OrderElement)
super(Order, self).__setitem__(item)


I didn't check your module condition since it isn't quite clear to me, but
you could add a second condition two Order class.


2011/8/7 Steven D'Aprano said:
Roy said:
I'm looking for good ways to ensure that attributes are only writable
such that they retain the characteristics the class requires.

Sounds like you're trying to do
http://en.wikipedia.org/wiki/Design_by_contract. Which is not a bad
thing. But, I think a more pythonic way to implement this would be to
verify behaviors, not types.

I would start by writing a assert_invarient() method which validates the
object. I'm guessing all you really need is that you can index [0] and
[1] and get ints, so test for that. Something like:

def assert_invarient(self):
try:
assert isinstance(data[0], int)
assert isinstance(data[1], int)
except:
raise ValueError

Don't do that. assert is for testing program logic, not verifying data.
The
problem with assert is that the user can turn all assertions off, simply
by
launching Python with the -O switch. Your verification code then becomes:

def assert_invarient(self):
try:
pass
except:
raise ValueError

which is useless.

When should you use an assertion? If you've ever written code like this:

if condition:
do_something()
else:
# This should never happen. But you know what they say: code that
# can't happen, does!
raise RuntimeError('condition unexpectedly false')


that's a prime candidate for turning into an assertion:


assert condition, 'condition unexpectedly false'
do_something()
 
S

Steven D'Aprano

John said:
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.

Below is a what I eventually came up with; a container class called
OrderElement for the inner lists, and subclass of list called Order for
the main attribute, which is a property of the main class, simplified
below as SeqSim. It works, but seems like a hell of a lot of code for a
simple idea.

And why should this be surprising? It might be a simple *idea*, but the
concrete execution of that idea is anything but simple. "Hey, let's fly to
Mars!" is a simple idea too.

Nevertheless, it does appear that your solution below is overly complicated.
Two helper classes just to have a thing that holds two ints... does it have
to be a list? Seems that if you're limited to exactly two items, a list is
pretty useless, since you can't insert, append, pop or delete items.

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


There's a little bit of code duplication there, but it's 3am here and I'm
tired and besides, if I do all your work what would you do? *wink*

This gives you an object that holds two integers. You can access them either
by attribute name, "a" and "b", or by index, 0 and 1:

instance = ThingWithTwoIntegers(23, 42)
instance[0]
=> 23
instance.b
=> 42

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?
 
J

John O'Hagan

That's what properties are for.


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

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

Forum statistics

Threads
473,744
Messages
2,569,484
Members
44,903
Latest member
orderPeak8CBDGummies

Latest Threads

Top