3

I've got an object that contains a large data table called _X. Data examples of various lengths sit stacked end-to-end inside _X, and a different table I've named _INDEX encodes the mapping from example number -> range in _X where that example lives.

What I want is to define a property called X with __getitem__ and __setitem__ of its own such that I can use X[i,j] to access the jth element of the ith example. This is to avoid having to write confusing verbose lines like self._X[self._INDEX[i]:self._INDEX[i+1]][j] all over the place.

I could make a wrapper class with the right __getitem__ and __setitem__ and return that from my @property function, but I'd rather not have to do that.

Pavel Komarov
  • 1,153
  • 12
  • 26
  • 1
    What does the property actually return? The property itself doesn't need to define `__getitem__`, just the class whose instance the property returns. – chepner Aug 12 '19 at 21:47
  • That is, `foo.X[3,4]` is really `type(foo).X.fget(foo)[3,4]`; whatever `X.fget` returns is what gets indexed, not `X` itself. – chepner Aug 12 '19 at 21:49
  • If you write a function named `X` containing the one verbose line, the rest of your code simply calls `a.X(i,j)`. Why doesn't that work? You can't avoid writing that verbose line once. No properties or special methods are needed. – Paul Cornelius Aug 12 '19 at 21:58
  • This makes me appreciate JavaScript, where I could always just overwrite whatever methods were bound. – Pavel Komarov Aug 12 '19 at 22:00
  • I don't want this to be a function, though that would effectively do the same thing. I want it to look like proper indexing to the user. This is one of several tables, and I can expose the others without any special functions. For symmetry I want this to behave just like one of those. – Pavel Komarov Aug 12 '19 at 22:01

2 Answers2

2

Define a wrapper class first that defines the desired __getitem__.

class Foo:
    def __init__(self, table, index):
        self.table = table
        self.index = index
    def __getitem__(self, k):
        i, j = k
        return self.table[self.index[i]:self.index[i+1]][j]

Then define your property

@property
def X(self):
    return Foo(self._X, self._INDEX)
chepner
  • 497,756
  • 71
  • 530
  • 681
  • Is there no cleaner way? I really don't want a whole new class. I really just want to override/intercept the `__getitem__` and `__setitem__` calls on my `._X` array. https://stackoverflow.com/questions/51159611/intercepting-getitem-calls-on-an-object-attribute – Pavel Komarov Aug 12 '19 at 22:10
  • You don't have *a* array; you have an object of some type that defines `__getitem__`, and that object stores objects of *another* type with its own `__getitem__`. – chepner Aug 12 '19 at 22:37
  • This doesn't really have to be a property, no? It can just be an attribute. – juanpa.arrivillaga Aug 12 '19 at 22:48
  • IIUC, the property is necessary to return a value whose type has the appropriate `__getitem__` defined, rather then the "original" value. – chepner Aug 12 '19 at 22:51
  • I mean just `self.x = Foo()`, then `obj.x[x,y]` would work, no? – juanpa.arrivillaga Aug 12 '19 at 22:59
  • @juanpa.arrivillaga Hm, I suppose so (though `Foo` still has to take `self._X` and `self._INDEX`, or just `self` itself, as arguments). I guess it's a trade-off between the memory used to keep a `Foo` instance around vs the cost of creating the `Foo` instance on demand. – chepner Aug 13 '19 at 02:48
  • @chepner I mean, the memory overhead is negligible, unless you are creating millions of instances of the class. In any case, you're still creating a `property` object, which has it's own memory overhead... – juanpa.arrivillaga Aug 13 '19 at 02:49
0

Add a function to your class and call it instead of __setitem__

def get_(self,attr,i,j):
    Attr = getattr(self.__class__,attr).__get__(self)
    return Attr[self._INDEX[i]:self._INDEX[i+1]][j]
def set_(self,attr,i,j,value):
    Attr = getattr(self.__class__,attr).__get__(self)
    a, a[self._INDEX[i]:self._INDEX[i+1]][j] = Attr, _ = Attr, value
    getattr(self.__class__,attr).__set__(self, Attr)

element = foo.get_('X',i,j)
foo.set_('X',i,j,value)

The main issue is that foo.X[i,j] = bar calls __get__ on the property instance even if you subclass it and add a __getitem__ or __setitem__ method. It then calls __setitem__ on the return value. Since the return value doesn't know about the property instance, it can't then call its __set__ method directly after.

Alternatively, if you're willing to do another call explicitly, you can just actually call the __set__ method right after.

foo.X[self._INDEX[i]:self._INDEX[i+1]][j] = bar
foo.X = foo._X

The first line only modifies foo._X so to call the setter you have to do so explicitly.

EDIT:

If the X setter and getter force _X to be instances of this class, item assignment will mutate _X and then call the setter.

class List(list):
    def __init__(self, obj, attr, lst):
        self.obj = obj
        self.attr = attr
        super().__init__(lst)
    def __setitem__(self, index, value):
        super().__setitem__(index, value)
        setattr(self.obj, self.attr, self)
Ryan Chou
  • 23
  • 4