Is there a way to make a object subscriptable? - python

I have an example class:
class collection:
def __init__(self, itemArray):
self.itemArray = itemArray
self.max = len(itemArray)
def __iter__(self):
self.index = 0
return self
def __next__(self):
if self.index < self.max:
result = self.itemArray[self.index]
self.index += 1
return result
else:
raise StopIteration()
My goal is to access the variable self.itemArray without having to explicitly use collection.itemArray from outside the class. I want to be able to loop over the object by making it an iterable, which is why I have __iter__ and __next__.
I want to mimic the behaviour that string types employs, ie.
stringVar = "randomTextString"
stringVar[indexVal]
Attempting this with a object would not work as it will raise a TypeError because objects aren't subscriptable.
I just need someone to point me in the right direction. I looked at the python docs for a solution but I didn't seem to find anything.

Override the __getitem__ and __setitem__ magics:
def __getitem__(self, idx):
return self.itemArray[idx]
def __setitem__(self, idx, val):
self.itemArray[idx] = val

Related

Enable Python Class to support for loop through an internal iterable member variable

from sortedcontainers import SortedSet
class BigSet(object):
def __init__(self):
self.set = SortedSet()
self.current_idx = -1
def __getitem__(self, index):
try:
return self.set[index]
except IndexError as e:
print('Exception: Index={0} len={1}'.format(index, len(self.ord_set)))
raise StopIteration
def add(self, element):
self.set.add(element)
def __len__(self):
return len(self.set)
def __iter__(self):
self.current_idx = -1
return self
def __next__(self):
self.current_idx += 1
if self.current_idx == len(self.set):
raise StopIteration
else:
return self.set[self.current_idx]
def main():
big = BigSet()
big.add(1)
big.add(2)
big.add(3)
for b in big:
print(b)
for b2 in big:
print(b2)
if __name__ == "__main__":
main()
I have a class that embeds an iterable member variable named self.set and I would like to enable this class to support for loop. The above is the code that I wrote for the purpose. However, I think there must be a better way to do this task easier since the class has an iterable member already.
Question> Is there a way that I can delegate the job to the embedded self.set? Also, I think there maybe a good way to implement the __getitem__ too.
Thank you
I am almost certain you do not want your container to be an iterator. So, it should not implement __next__. Instead, it should be iterable, so it only needs to implement __iter__. In that case, if you want to delegate to the iterable member:
def __iter__(self):
return iter(self.set)
And remove your __next__ method.

len() function not working properly in Python

I have the following code:
class Stat(list):
def __init__(self, lst = []):
self.s = list(lst)
def __repr__(self):
return "Stat({})".format(self.s)
def add(self, item):
self.s.append(item)
def len(self):
return len(self.s)
...(more methods, but not necessary)
All of the methods work properly but len(). No matter the length of the Stat object, the returned length is always 0; I don't understand why.
it will return 0 always when you are using it like this:
x = Stat([1,3,4,5])
print len(x)
if you want to override len function use this code:
def __len__(self):
return len(self.s)
s = Stat([1, 2])
s.add(1)
s.add(2)
print s.len()
I have run your code, the result is correct in my environment.
Override the magic method __len__(self) to control the output of a call to len(my_stat_object):
class Stat(list):
def __init__(self, lst = []):
self.s = list(lst)
def __repr__(self):
return "Stat({})".format(self.s)
def add(self, item):
self.s.append(item)
def __len__(self):
return len(self.s)
If what you're trying to run is len(stat) and not stat.len(), your function len should be named __len__ instead. Here's the docs: object.len
stat = Stat([1, 2])
len(s) # 0 if len, 2 if __len__
As a side note, you might want to replace lst=[] in your init definition, as it can cause some weird looking behaviours. Read about it here: mutable default argument

__iter__ (iterator and generator)

I read a book about advanced topics in python.
The author was trying to explain the generator.
This was his example to explain:
class rev:
def __init__(self,data):
self.data = data
self.index = len(data)
def __iter__(self):
return self
def __next__(self):
if self.index == 0:
raise StopIteration
self.index = self.index - 1
return self.data[self.index]
def main():
reve = rev('zix')
for i in reve:
print(i)
if __name__=='__main__':
main()
The main idea of this code is to reverse generators.
The output is :
x
i
z
The thing I found hard to understand is this part:
def __iter__(self):
return self
Can someone explain it to me?
When you do for x in xs, xs has to be an iterable, which means you can get an iterator out of it by iter(xs), which you can do when xs.__iter__() is implemented. An iterator is required to implement __next__(), so that the in operator can consume it one by one by calling next() on it.
Now, in your case
reve = rev("hello") # is an iterable, as there is rev.__iter__()
rev1 = iter(reve) # is an iterator, this is what rev.__iter__() returns
rev2 = next(rev1) # now go on calling next() till you get StopIteration
Type the above snippet in REPL. Run it a few times. You will get a feel for it.
The iterator protocol is comprised of two methods:
__iter__, and
__next__
Also, a requirement is that __iter__ returns self -- so if you have an obj that is an iterator then
obj is iter(obj) is obj.__iter__()
is true.
This is a good thing because it allows us to say iter = iter(obj) and if obj was already an iterator we still have the same object.
Since your class provides an implementation for next() it returns self so the caller will call that when looping over your object. In contrast, if you only wanted to wrap a data structure that already provides an implementation of __iter__ and next (e.g. list), you could return self.data.__iter__() from your class. In that case the caller would call next() defined on that object when doing a loop or list comprehension let's say.
class rev:
def __init__(self, data):
self.data = data
self.index = len(data)
def __iter__(self):
return self
def next(self):
if self.index == 0:
raise StopIteration
self.index = self.index - 1
return self.data[self.index]
class rev2:
def __init__(self, data):
self.data = data
def __iter__(self):
return self.data.__iter__()
def main():
for i in rev('zix'): # str doesn't provide implementation for __iter__
print(i)
for i in rev2(['z', 'i', 'x']): # list does so no need to implement next
print(i)

Implicitly binding callable objects to instances

I have this code:
class LFSeq: # lazy infinite sequence with new elements from func
def __init__(self, func):
self.evaluated = []
self.func = func
class __iter__:
def __init__(self, seq):
self.index = 0
self.seq = seq
def next(self):
if self.index >= len(self.seq.evaluated):
self.seq.evaluated += [self.seq.func()]
self.index += 1
return self.seq.evaluated[self.index - 1]
And I explicitely want that LFSeq.__iter__ becomes bounded to an instance of LFSeq like any other user-defined function would have been.
It doesn't work this way though because only user-defined functions are bounded and not classes.
When I introduce a function decorator like
def bound(f):
def dummy(*args, **kwargs):
return f(*args, **kwargs)
return dummy
then I can decorate __iter__ by it and it works:
...
#bound
class __iter__:
...
This feels somehow hacky and inconsistent however. Is there any other way? Should it be that way?
I guess yes because otherwise LFSeq.__iter__ and LFSeq(None).__iter__ wouldn't be the same object anymore (i.e. the class object). Maybe the whole thing about bounded functions should have been syntactic sugar instead of having it in the runtime. But then, on the other side, syntactic sugar shouldn't really dependent on content. I guess there has to be some tradeoff at some place.
The easiest solution for what you are trying to do is to define your __iter__() method as a generator function:
class LFSeq(object):
def __init__(self, func):
self.evaluated = []
self.func = func
def __iter__(self):
index = 0
while True:
if index == len(self.evaluated):
self.evaluated.append(self.func())
yield self.evaluated[index]
index += 1
Your approach would have to deal with lots of subtleties of the Python object model, and there's no reason to go that route.
In my opinion, the best solution is #Sven one, no doubt about it. That said, what you are trying to do really seems extremely hackish - I mean, to define __iter__ as a class. It will not work because declaring a class inside another one is not like defining a method, but instead it is like defining an attribute. The code
class LFSeq:
class __iter__:
roughly equivalent to an attribution that will create a class field:
class LFSeq:
__iter__ = type('__iter__', (), ...)
Then, every time you define an attribute inside a class, this is bound to the class itself, not to specific instances.
I think you should follow #Sven solution, but if you really want to define a class for any other reason, it seems you are lucky, because your generator class does not depend upon nothing from the LFSeq instance itself. Just define the iterator class outside:
class Iterator(object):
def __init__(self, seq):
self.index = 0
self.seq = seq
def next(self):
if self.index >= len(self.seq.evaluated):
self.seq.evaluated += [self.seq.func()]
self.index += 1
return self.seq.evaluated[self.index - 1]
and instantiate it inside LFSeq.__iter__() method:
class LFSeq(object): # lazy infinite sequence with new elements from func
def __init__(self, func):
self.evaluated = []
self.func = func
def __iter__(self):
return Iterator(self)
If you eventually need to bind the iterator class to the instance, you can define the iterator class inside LFSeq.__init__(), put it on a self attribute and instantiate it in LFSeq.__iter__():
class LFSeq(object): # lazy infinite sequence with new elements from func
def __init__(self, func):
lfseq_self = self # For using inside the iterator class
class Iterator(object): # Iterator class defined inside __init__
def __init__(self):
self.index = 0
self.seq = lfseq_self # using the outside self
def next(self):
if self.index >= len(self.seq.evaluated):
self.seq.evaluated += [self.seq.func()]
self.index += 1
return self.seq.evaluated[self.index - 1]
self.iterator_class = Iterator # setting the itrator
self.evaluated = []
self.func = func
def __iter__(self):
return self.iterator_class() # Creating an iterator
As I have said, however, #Sven solution seems finer. I just answered do try to explain why your code did not behaved as you expected and to provide some info about to do what you want to do - which may be useful sometimes nonetheless.

How does this class implement the "__iter__" method without implementing "next"?

I have the following code in django.template:
class Template(object):
def __init__(self, template_string, origin=None, name='<Unknown Template>'):
try:
template_string = smart_unicode(template_string)
except UnicodeDecodeError:
raise TemplateEncodingError("Templates can only be constructed from unicode or UTF-8 strings.")
if settings.TEMPLATE_DEBUG and origin is None:
origin = StringOrigin(template_string)
self.nodelist = compile_string(template_string, origin)
self.name = name
def __iter__(self):
for node in self.nodelist:
for subnode in node:
yield subnode
def render(self, context):
"Display stage -- can be called many times"
return self.nodelist.render(context)
The part I am confused about is below. How does this __iter__ method work? I can't find any corresponding next method.
def __iter__(self):
for node in self.nodelist:
for subnode in node:
yield subnode
This is the only way that I know how to implement __iter__:
class a(object):
def __init__(self,x=10):
self.x = x
def __iter__(self):
return self
def next(self):
if self.x > 0:
self.x-=1
return self.x
else:
raise StopIteration
ainst = a()
for item in aisnt:
print item
In your answers, try to use code examples rather than text, because my English is not very good.
From the docs:
If a container object’s __iter__()
method is implemented as a generator,
it will automatically return an
iterator object (technically, a
generator object) supplying the
__iter__() and __next__() methods.
Here is your provided example using a generator:
class A():
def __init__(self, x=10):
self.x = x
def __iter__(self):
for i in reversed(range(self.x)):
yield i
a = A()
for item in a:
print(item)
That __iter__method returns a python generator (see the documentation), as it uses the yield keyword.
The generator will provide the next() method automatically; quoting the documentation:
What makes generators so compact is that the __iter__() and next() methods are created
automatically.
EDIT:
Generators are really useful. If you are not familiar with them, I suggest you readup on them, and play around with some test code.
Here is some more info on iterators and generators from StackOverflow.

Categories

Resources