2

The class that acts as my starting point is as follows:

class Test:
    def __getitem__(self, key):
        global frame
        frame = inspect.currentframe()
        if key > 9:
            raise KeyError
        return key

My thought was to use frame.f_back to discover that an iterator was automatically created for an instance as in the following example:

for x in Test():
    x

After running both of these and looking at frame.f_back, it was not obvious if __getitem__ can get enough information to detect if it is being called from whatever "iterator" that is interacting with it. The easiest solution would be to make the container start at one instead of zero to access its contents or maybe even to force wrapping the key in a list before passing it to the function as shown here:

>>> class Test:
    def __getitem__(self, key):
        if not isinstance(key, list):
            raise TypeError
        if len(key) != 1:
            raise ValueError
        key = key.pop()
        if not isinstance(key, int):
            raise TypeError
        if not 0 <= key < 10:
            raise KeyError
        return key


>>> for x in Test():
    x


Traceback (most recent call last):
  File "<pyshell#42>", line 1, in <module>
    for x in Test():
  File "<pyshell#39>", line 4, in __getitem__
    raise TypeError
TypeError
>>> Test()[[5]]
5
>>> 

Is there a way that __getitem__ can know that it is being used automatically as an iterator and raise an exception to prevent such usage?

Related: Why does defining __getitem__ on a class make it iterable in python?

Noctis Skytower
  • 21,433
  • 16
  • 79
  • 117
  • This question provides [a Minimal, Reproducible Example](https://stackoverflow.com/help/minimal-reproducible-example) as required and is part of a larger problem. This is not part of a [XY problem](https://meta.stackexchange.com/q/66377/148746). – Noctis Skytower Nov 27 '19 at 14:27
  • If your question is part of a larger problem, it's very possible that it *is* an XY problem. Does the problem very specifically requires you to implement the limitation on `__getitem__`? Having that extra bit of insight can be helpful. Regardless, does the question boil down to not allowing the object to iterate? – r.ook Nov 27 '19 at 14:38
  • @r.ook Yes, it _could_ be an XY problem but has been determined to _not_ be an XY problem. The code is part of a class that is designed for teaching about arrays as one might work with them in C. The class defines `__getitem__`, `__setitem__`, and no `__len__`. Yes, the object should not be iterable. The two methods could be renamed to `get_value` and `set_value`, but my hope was to keep the usual array syntax. – Noctis Skytower Nov 27 '19 at 14:45
  • In that scenario, wouldn't a `def __iter__(self): raise TypeError('Object is not iterable')` suffice? `for x in foo` will return `iter(foo)` first, which raises the error. But the `__getitem__` can still be called individually. – r.ook Nov 27 '19 at 14:49
  • @r.ook Thank you! If you add that as an answer here, I will accept it. – Noctis Skytower Nov 27 '19 at 14:52

2 Answers2

2

If the goal is to stop the object from being an iterable, you can just force an error on __iter__ method:

class Test:
    def __getitem__(self, key):
        if key > 9:
            raise KeyError
        return key
    def __iter__(self):
        raise TypeError('Object is not iterable')

Test run:

>>> t = Test()
>>> for x in t:
    print(x)

Traceback (most recent call last):
  File "<pyshell#126>", line 1, in <module>
    for x in t:
  File "<pyshell#122>", line 7, in __iter__
    raise TypeError('Object is not iterable')
TypeError: Object is not iterable

But __getitem__ will still work:

>>> t[0]
0
>>> t[1]
1
>>> t[10]
Traceback (most recent call last):
  File "<pyshell#129>", line 1, in <module>
    t[10]
  File "<pyshell#122>", line 4, in __getitem__
    raise KeyError
KeyError
r.ook
  • 13,466
  • 2
  • 22
  • 39
  • Fun fact: There is also an link to answer included in the question. – Lee Nov 27 '19 at 15:00
  • Yes, I think OP was too pigeon-holed in implementing a fix in `__getitem__` so that answer unfortunately didn't pop out to them. – r.ook Nov 27 '19 at 15:05
  • 1
    @r.ook You are right. It was not intuitive to me to define an "unrelated" method and raise an exception there. The answer is obvious now, but many things are in hindsight. – Noctis Skytower Nov 27 '19 at 15:28
2

The way to do this is implementing both __getitem__ and __iter__ methods. When trying to acces individual elements __getitem__ will be triggered, when trying to iterate __iter__ will be called and an exception raised.

class Test():
    def __getitem__(self, k):
        return k

    def __iter__(self):
        raise TypeError
Happy-Monad
  • 1,962
  • 1
  • 6
  • 13