@property
is a nice way to define getters. When the property is mutable, the reference returned can be used to modify the property in ways not controlled by the class definition. I'll use a banana stand as a motivating analogy, but this issue applies to any class that wraps a container.
class BananaStand:
def __init__(self):
self._money = 0
self._bananas = ['b1', 'b2']
@property
def bananas(self):
return self._bananas
def buy_bananas(self, money):
change = money
basket = []
while change >= 1 and self._bananas:
change -= 1
basket.append(self._bananas.pop())
self._money += 1
return change, basket
I would like visitors to the banana stand to pay for their bananas. Unfortunately, there's nothing stopping a monkey (who doesn't know any better) from taking one of my bananas. The monkey didn't have to use the internal attribute _banana
, they just took a banana without paying.
def take_banana(banana_stand):
return banana_stand.bananas.pop()
>>> stand = BananaStand()
>>> stand.bananas
['b1', 'b2']
>>> take_banana(stand)
'b2'
>>> stand.bananas
['b1']
This analogy is a little silly, but any class that has mutable attributes is not protected from accidental vandalism. In my actual case, I have a class with two array attributes that must remain the same length. With array, there's nothing stopping a user from splicing a second array into the first and silently breaking my equal size invariant:
>>> from array import array
>>> x = array('f', [1,2,3])
>>> x
array('f', [1.0, 2.0, 3.0])
>>> x[1:2] = array('f', [4,5,6])
>>> x
array('f', [1.0, 4.0, 5.0, 6.0, 3.0])
This same behavour occurs when the array is a property.
I can think of two ways of avoiding issue:
__setitem__
. I am resistant to this because I would like to be able to use this array splicing behaviour internally.Is there an elegant way around this problem? I'm particularly interested in fancy ways of subclassing property.
The two ways you proposed are both good ideas. Let me throw in one more: tuples! Tuples are immutable.
@property
def bananas(self):
return tuple(self._bananas)
Now that you have these alternative, there are a couple of things of things to keep in mind while choosing one over the other:
list
is falling short on? Subclass a list and raise exceptions on mutating functions. [1][1]: jsbueno has a nice ReadOnlyList
implementation that doesn't have the O(n) overhead.
It took me a long time, but I think I've created a pretty robust and flexible solution based on the recipe provided in this answer. With great pride, I present the FixLen
wrapper:
from array import array
from collections import MutableSequence
from inspect import getmembers
class Wrapper(type):
__wraps__ = None
__ignore__ = {
'__class__', '__mro__', '__new__', '__init__', '__dir__',
'__setattr__', '__getattr__', '__getattribute__',}
__hide__ = None
def __init__(cls, name, bases, dict_):
super().__init__(name, bases, dict_)
def __init__(self, obj):
if isinstance(obj, cls.__wraps__):
self._obj = obj
return
raise TypeError(
'wrapped obj must be of type {}'.format(cls.__wraps__))
setattr(cls, '__init__', __init__)
@property
def obj(self):
return self._obj
setattr(cls, 'obj', obj)
def __dir__(self):
return list(set(dir(self.obj)) - set(cls.__hide__))
setattr(cls, '__dir__', __dir__)
def __getattr__(self, name):
if name in cls.__hide__:
return
return getattr(self.obj, name)
setattr(cls, '__getattr__', __getattr__)
for name, _ in getmembers(cls.__wraps__, callable):
if name not in cls.__ignore__ \
and name not in cls.__hide__ \
and name.startswith('__') \
and name not in dict_:
cls.__add_method__(name)
def __add_method__(cls, name):
method_str = \
'def {method}(self, *args, **kwargs):\n' \
' return self.obj.{method}(*args, **kwargs)\n' \
'setattr(cls, "{method}", {method})'.format(method=name)
exec(method_str)
class FixLen(metaclass=Wrapper):
__wraps__ = MutableSequence
__hide__ = {
'__delitem__', '__iadd__', 'append', 'clear', 'extend', 'insert',
'pop', 'remove',
}
# def _slice_size(self, slice):
# start, stop, stride = key.indices(len(self.obj))
# return (stop - start)//stride
def __setitem__(self, key, value):
if isinstance(key, int):
return self.obj.__setitem__(key, value)
#if self._slice_size(key) != len(value):
if (lambda a, b, c: (b - a)//c)(*key.indices(len(self.obj))) \
!= len(value):
raise ValueError('input sequences must have same length')
return self.obj.__setitem__(key, value)
FixLen
keeps an internal reference to the mutable sequence that you pass to its constructor and blocks access to, or provides an alternate definition of methods that change the length of the object. This allows me to mutate the length internally, but protect the length of the sequence from modification when passed as a property. It's not perfect (FixLen
should subclass Sequence
, I think).
Example usage:
>>> import fixlen
>>> x = [1,2,3,4,5]
>>> y = fixlen.FixLen(x)
>>> y
[1, 2, 3, 4, 5]
>>> y[1]
2
>>> y[1] = 100
>>> y
[1, 100, 3, 4, 5]
>>> x
[1, 100, 3, 4, 5]
>>> y.pop()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'NoneType' object is not callable
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With