4

I've got a code:

def constant(f):
    def fset(self, value):
        raise SyntaxError( "You can't change a constant!" )
    def fget(self):
        return f(self)
    return property(fget, fset)

class A( object ):
    def __init__( self, a_list ):
        super( A, self ).__init__()
        self.__data = a_list[:]
    @constant
    def data( self ):
        return self.__data

I would like to make self.__data accessible, but unchangeable and uneditable from outside of the class. I can't simply change it to a tuple, cause I would like it to be easy to change for methods inside the class. The solution that I presented above doesn't work, when someone modifies the list:

>>> a = A( range(5) )
>>> a.data = []
Traceback (most recent call last):
    ...
SyntaxError: You can't change a constant! # it works
>>> a.data[0]=6                           # it doesn't work
>>> a.data
[6, 1, 2, 3, 4]

Do you know any nice solution of my problem?

user1633361
  • 141
  • 9
  • 1
    Possible duplicate of http://stackoverflow.com/questions/2682745/creating-constant-in-python – AlG Aug 27 '15 at 15:52
  • It's python, there is always a way to change it. If people use your code incorrectly it will break and it's on them. Just make sure you have some good documentation stating why you shouldn't change it. – CasualDemon Aug 27 '15 at 15:54
  • 1
    So you want it to appear to be externally, but actually internally not be? Why? *"We're all consenting adults, here"* - just a single leading underscore on the list should suffice. – jonrsharpe Aug 27 '15 at 15:56
  • 1
    The other option is to make `data` `return self.__data[:]`, then if whatever receives it does mutate it that doesn't matter. – jonrsharpe Aug 27 '15 at 15:58
  • 2
    Consider raising a custom exception rather than a `SyntaxError` as it's not really a syntax problem if someone tries to use your object in a way you don't like. – Two-Bit Alchemist Aug 27 '15 at 18:27
  • Why not `self.__data = tuple(a_list)`? – Cristian Ciupitu Aug 27 '15 at 18:36
  • 1
    @CristianCiupitu Because the idea is to retain mutability internally. The other class methods are supposed to be able to change the internal list, but nothing outside the class. (This is fundamentally about a sort of encapsulation that Python just does not support.) – Two-Bit Alchemist Aug 27 '15 at 18:54
  • What about the case where the elements of the list are mutable, for example they're lists or dictionaries? – Cristian Ciupitu Aug 27 '15 at 19:24
  • @jonrsharpe I thought there is a smart solution, that allows encapsulation. Your solution (return copy of the list) will still fail, when the elements of list are mutable, as Cristian Ciupitu said (but using a tuple in the situation doesn't change anything, you can still modify a mutable elements of the tuple). But maybe copy.deepcopy() will work, although it is not efficient. – user1633361 Aug 28 '15 at 08:16

2 Answers2

1

Provide some hints what shouldn't be changed but let people change it if they really want.

If you want uneditable list use tuple. Use leading underscore for extra "security".

There's always way to change list anything in Python. Even if I had to break into your house, steal your code and then change it - it's possible.

_change_this_and_I_will_kill_you_with_my_axe = [] # never use this name

If you eg. return list and don't want it modified return copy but there still will be way to modify it.

return my_list[:]
David Mašek
  • 913
  • 8
  • 23
  • 1
    "Even if I had to break into your house, steal your code, and change it." If changing the source code counts, I don't think your tuple suggestion is going to fare any better. :P – Two-Bit Alchemist Aug 27 '15 at 18:19
  • @Two-BitAlchemist Yes, you could change it to `list` and mutate that. :) In case it isn't obvious the point isn't to prevent your code from changing but to provide hints - _maybe you shouldn't change my variable `pi` to 42, but feel free to do so if you really want._ – David Mašek Aug 27 '15 at 18:27
1

You can also create a View-class that implements all the read operations for the list. Of course, there would still be a way to access the internal list, there is no was to prevent that.

class ListView(object):
  def __init__(self, list_):
    self._list = list_
  def __getitem__(self, index):
    return self._list[index]
  def __repr__(self):
    return 'ListView(%r)' % self._list
  def index(self, value):
    return self._list.index(value)
  # and so on

(sent from a mobile device)

Niklas R
  • 16,299
  • 28
  • 108
  • 203
  • You can do this, but be prepared to write a couple dozen redundant methods that do one of two things: defer to the internal list (as each method you've shown does), or raise an exception complaining about an invalid op. – Two-Bit Alchemist Aug 27 '15 at 18:56
  • 2
    I would suggest using `collections.abc.Sequence` as the base class. All you need do is define `__getitem__` and `__len__` and the `abc` will do the rest for you. – Dunes Aug 27 '15 at 19:02