Search code examples
pythonoopfile-ioiterablepython-collections

Subclassing a file object to "fake" it as an iterable - Python


My thought was to get rid of how users are constantly using seek(0) to reset the text file reading.

So instead I've tried to create a MyReader that's an collections.Iterator and then using .reset() to replace seek(0) and then it continues from where it last yielded by retaining a self.iterable object.

class MyReader(collections.Iterator):
    def __init__(self, filename):
        self.filename = filename
        self.iterable = self.__iterate__()

    def __iterate__(self):
        with open(self.filename) as fin:
            for line in fin:
                yield line.strip()

    def __iter__(self):
        for line in self.iterable:
            yield line

    def __next__(self):
        return next(self.iterable)

    def reset(self): 
        self.iterable = self.__iterate__()

The usage would be something like:

$ cat english.txt
abc
def
ghi
jkl

$ python

>>> data = MyReader('english.txt')
>>> print(next(data))
abc
>>> print(next(data))
def
>>> data.reset()
>>> print(next(data))
abc

My question is does this already exist in Python-verse somewhere? Esp. if there's already a native object that does something like this, I would like to avoid reinventing the wheel =)

If it doesn't exist? Does the object look a little unpythonic? Since it says it's an iterator but the true Iterator is actually the self.iterable and the other functions are wrapping around it to do "resets".


Solution

  • I think it depends on what is your real situation. Let's say if you just want to get rid of file.seek(0), it can be simple:

    class MyReader:
        def __init__(self, filename, mode="r"):
            self.file = open(filename, mode)
    
        def __enter__(self):
            return self
    
        def __exit__(self, exc_type, exc_value, traceback):
            self.close()
    
        def __iter__(self):
            self.file.seek(0)
            for line in self.file:
                yield line.strip()
    
        def close(self):
            self.file.close()
    

    You can even use it like a normal context manager:

    with MyReader("a.txt") as a:
        for line in a:
            print(line)
        for line in a:
            print(line)
    

    output:

    sdfas
    asdf
    asd
    fas
    df
    asd
    f
    sdfas
    asdf
    asd
    fas
    df
    asd
    f