On Sat, Nov 07, 2015 at 12:53:11PM +0000, Albert-Jan Roskam wrote: [...] > Ok, now to my question. I want to create a class with read-only > attribute access to the columns of a .csv file. E.g. when a file has a > column named 'a', that column should be returned as list by using > instance.a. At first I thought I could do this with the builtin > 'property' class, but I am not sure how.
90% of problems involving computed attributes (including "read-only" attributes) are most conveniently solved with `property`, but I think this may be an exception. Nevertheless, I'll give you a solution in terms of `property` first. I'm too busy/lazy to handle reading from a CSV file, so I'll fake it with a dict of columns. class ColumnView(object): _data = {'a': [1, 2, 3, 4, 5, 6], 'b': [1, 2, 4, 8, 16, 32], 'c': [1, 10, 100, 1000, 10000, 100000], } @property def a(self): return self._data['a'][:] @property def b(self): return self._data['b'][:] @property def c(self): return self._data['c'][:] And in use: py> cols = ColumnView() py> cols.a [1, 2, 3, 4, 5, 6] py> cols.a = [] Traceback (most recent call last): File "<stdin>", line 1, in ? AttributeError: can't set attribute Now, some comments: (1) You must inherit from `object` for this to work. (Or use Python 3.) It won't work if you just say "class ColumnView:", which would make it a so-called "classic" or "old-style" class. You don't want that. (2) Inside the property getter functions, I make a copy of the lists before returning them. That is, I do: return self._data['c'][:] rather than: return self._data['c'] The empty slice [:] makes a copy. If I did not do this, you could mutate the list (say, by appending a value to it, or deleting items from it) and that mutation would show up the next time you looked at the column. (3) It's very tedious having to create a property for each column ahead of time. But we can do this instead: def make_getter(key): def inner(self): return self._data[key][:] inner.__name__ = key return property(inner) class ColumnView(object): _data = {'a': [1, 2, 3, 4, 5, 6], 'b': [1, 2, 4, 8, 16, 32], 'c': [1, 10, 100, 1000, 10000, 100000], } for key in _data: locals()[key] = make_getter(key) del key and it works as above, but without all the tedious manual creation of property getters. Do you understand how this operates? If not, ask, and someone will explain. (And yes, this is one of the few times that writing to locals() actually works!) (4) But what if you don't know what the columns are called ahead of time? You can't use property, or descriptors, because you don't know what to call the damn things until you know what the column headers are, and by the time you know that, the class is already well and truly created. You might think you can do this: class ColumnView(object): def __init__(self): # read the columns from the CSV file self._data = ... # now create properties to suit for key in self._data: setattr(self, key, property( ... )) but that doesn't work. Properties only perform their "magic" when they are attached to the class itself. By setting them as attributes on the instance (self), they lose their power and just get treated as ordinary attributes. To be technical, we say that the descriptor protocol is only enacted when the attribute is found in the class, not in the instance. You might be tempted to write this instead: setattr(self.__class__, key, property( ... )) but that's even worse. Now, every time you create a new ColumnView instance, *all the other instances will change*. They will grown new properties, or overwrite existing properties. You don't want that. Fortunately, Python has an mechanism for solving this problem: the `__getattr__` method and friends. class ColumnView(object): _data = {'a': [1, 2, 3, 4, 5, 6], 'b': [1, 2, 4, 8, 16, 32], 'c': [1, 10, 100, 1000, 10000, 100000], } def __getattr__(self, name): if name in self._data: return self._data[name][:] else: raise AttributeError def __setattr__(self, name, value): if name in self._data: raise AttributeError('read-only attribute') super(ColumnView, self).__setattr__(name, value) def __delattr__(self, name): if name in self._data: raise AttributeError('read-only attribute') super(ColumnView, self).__delattr__(name) -- Steve _______________________________________________ Tutor maillist - Tutor@python.org To unsubscribe or change subscription options: https://mail.python.org/mailman/listinfo/tutor