Understanding of generators and iterators in python

Keywords: Python unit testing

Before introducing generators and iterators, let's talk about__ iter__ () and iter() functions, and__ next__ () and next(), the iterator must understand these two functions.

After learning object-oriented, you should know the methods in the object to__ Start with__ The methods at the end are built-in properties, so here__ iter__ And__ next__ It is also defined in the class template. Generally, we do not call these two things directly in the external code.
So actually__ iter__ And iter() are functions that return iterator objects__ next__ And next() are the values that return the next element of the iterator object.
Just take__ Is used when defined in a class. This class implements the function of the iterator. Since the class has implemented the function of the iterator, the instantiated object is the iterator, so there is no need to call the iter() function.

As I understand it, iter() is next(), which is an encapsulated external function of python and can be provided to users. The former is used to generate an iterator, while the latter is used in conjunction with the iterator to output the next element of the iterator object.

If you want to define an iterator yourself, you can use it__ iter__ () and__ next__ () these two methods.

The above is about__ iter__ () and iter() functions, and__ next__ () and next() have a little personal understanding. I haven't understood it before.

Let's start with iterators.
1, Iterator
Before introducing iterators, let's first introduce the iteratable object. Iteration is literally a repeated action. The iteratable object repeatedly performs the same operation on this object. This action is generally to continuously read the next value in the object.
The definition of iteration found by Baidu is as follows: iteration is a ring structure. Starting from the initial state, each iteration traverses the ring and updates the state. It iterates many times until it reaches the end state.
If an object can be iterated, it must meet many elements. A single number, for example, cannot be operated repeatedly. Iteration is meaningless to it.
Therefore, strings, tuples, lists, dictionaries, and collections in python are all iteratable.

Iteratable objects need to be implemented in python__ iter__ () this method.
The reason why strings, tuples, lists, dictionaries and collections are iteratable objects is that python is defined at the bottom__ iter__ () this method, so it is an iterative object.

If it can be iterated, such as [1,2,3,4], the element value of the first iteration is 1. If you continue the iteration next time, how can you know where the next iteration is and remember where the last iteration is? Therefore, there is the concept of iterator, which is used to remember the location of the last iteration.

The iterator object needs to meet its class implementation__ iter__ () and__ next__ () these two methods.

from collections.abc import Iterable,Iterator
class MyNumber(object):

    def __init__(self):
        self.a = 0
    def __iter__(self):
        return self
    def __next__(self):
        self.a += 1
        if self.a >10:
            raise StopIteration
        return self.a

m = MyNumber()
print(isinstance(m,Iterable))
print(isinstance(m,Iterator))

Operation results:

True
True

If the above MyNumber class__ next__ () method is commented out, then the m object is not an iterator, but an iteratable object.

If you call iter(m) to generate an iterator, an error will be reported:

class MyNumber(object):

    def __init__(self):
        self.a = 0

    def __iter__(self):
        return self
        m = MyNumber()
print(isinstance(m,Iterable))
print(isinstance(m,Iterator))
mm = iter(m)

Operation results:

True
False
Traceback (most recent call last):
  File "C:/Users/10270/Desktop/py_test/test_10_3.py", line 44, in <module>
    mm = iter(m)
TypeError: iter() returned non-iterator of type 'MyNumber'

At first, I expected to be True, but I threw an exception because I thought that ITER () is an external function encapsulated in Python and can be executed and used. My m is already an iterative object. Why can't I call ITER () to generate an iterator? After the break point, I found that the user-defined function in MyNumber class will be called when executing iter(m) statement__ iter__ () method instead of calling the python external function iter(). What does this mean? When calling the ITER () function, python should also call the iteratable object__ iter__ The reason why tuples and lists can be called successfully is because of the internal__ iter__ Method is consistent with generating iterators, which I have defined in the class of iteratable object m here__ iter__ Method, it will call my own__ iter__ Method, which only returns a self and cannot meet the conditions for generating an iterator. The iterator needs to have__ next__ Method. After improving the above code, you can.

class MyNumber(object):
    def __init__(self):
        self.a = 0
    def __iter__(self):
        return B()
    # def __next__(self):
    #     self.a += 1
    #     if self.a >10:
    #         raise StopIteration
    #     return self.a
class B(MyNumber,object):
    def __next__(self):
        self.a += 1
        if self.a >10:
            raise StopIteration
        return self.a

m = MyNumber()
print(isinstance(m,Iterable))
print(isinstance(m,Iterator))
mm = iter(m)
print(isinstance(mm,Iterator))

Operation results:

True
False
True

So my guess is right.
The conditions for iterator generation are:
① Realized__ iter__ Methods and__ next__ Method, the object generated by this class must be an iterator. Even if nothing is done in the two methods (as shown in the following example).

class MyNumber(object):

    def __init__(self):
        self.a = 0

    def __iter__(self):
        pass
    def __next__(self):
        pass
m = MyNumber()
print(isinstance(m,Iterable))
print(isinstance(m,Iterator))

Operation results:

True
True

② If it does__ iter__ This method, but the returned object is an implementation__ next__ Method.

class MyNumber(object):
    def __init__(self):
        self.a = 0
    def __iter__(self):
        return B()
    # def __next__(self):
    #     self.a += 1
    #     if self.a >10:
    #         raise StopIteration
    #     return self.a
class B(MyNumber,object):
    def __next__(self):
        self.a += 1
        if self.a >10:
            raise StopIteration
        return self.a

m = MyNumber()
print(isinstance(m,Iterable))
print(isinstance(m,Iterator))
mm = iter(m)
print(isinstance(mm,Iterator))

return B() above is to instantiate an object of class B, which implements__ next__ Method, and inherits the of MyNumber__ iter__ method. So mm is satisfied__ iter__ And__ next__ method. So it's an iterator.

2, Generator
What is a generator, more abstract! The function that uses yield is the generator. The generator is also an iterator. A function is defined as a generator. If you want to use the data returned by yield, you need to call the next() function.

The difference between yield and return: after a function is called, return returns data directly, and the environment of the function will be recycled. However, after yield returns data, the environment of the function will remain. When the next is called, it will continue to return to the previous yield and execute there, and then execute the following statements until yield returns, and the environment will remain... Repeatedly, As long as you call next(), you will continue. If you encounter the next yield, you will throw a StopIteration exception.

def test():
    print("test")
    yield 1
    print("Happy birthday, mother of the motherland!")
    yield 2
    print("The second time")

try:
    f = test()
    print(type(f))
    next(f)
    next(f)
    next(f)
except StopIteration:
    print("End of iteration")

Operation results:

test
 Happy birthday, mother of the motherland!
The second time
 End of iteration

It can be seen from the above that there can be multiple yield s in a function, but if there are multiple returns, there will be no syntax errors and no practical significance. The statements after the first return are useless and will never be executed, because after the return, the function environment will be recycled and the next function call will start over.

To sum up: the generator is something that can continuously generate data as long as the user designs the code, and the environment for generating data next time is to generate data based on the environment for retaining the data generated last time.

There are two ways to implement the generator: one is user-defined functions, which need to be decorated with yield. Second, through the variation of list generation formula
List generating formula:
[x for x in range(10)]
This is a list that generates 0-9 [1,2,3,4,5,6,7,8,9]
Change the above to (x for x in range(10)) and assign it to g, then G is not only a generator, but also an iterator and an iterative object. You can use for to traverse, and you can use the next() function to make continuous calls to generate the data in the list.
Note: the following two examples are the pits I encountered in the process of learning. I understand the first example and I don't understand the second one, but I know how to change the code and won't report errors. I hope my friends who read this article can answer my questions.
Pit 1:

def test():
    count = 0
    while True:
        count += 1
        print(count)
        yield 1
        if count >= 3:
            return

try:
    # f = test()
    # print(type(f))
    next(test())
    next(test())
    next(test())
except StopIteration:
    print("End of iteration")

Operation results:

1
1
1

It turned out to be a collapse. Why didn't you do what I thought: return the following results

1
2
 End of iteration

The reason is: next(test()), test() here is different every time, so it is called again every time, not at the location of the last test() call.
Change the code in the above try to the following:

try:
    f = test()
    # print(type(f))
    next(f)
    next(f)
    next(f)
except StopIteration:
    print("End of iteration")

The above f is called once, and this f is used every time next().

Pit 2:

def test():
    count = 0
    while True:
        count += 1
        print(count)
        yield 1
        if count >= 2:
            raise StopIteration

try:
    f = test()
    # print(type(f))
    next(f)
    next(f)
    next(f)
except StopIteration:
    print("End of iteration")

Operation results:

1
2
Traceback (most recent call last):
  File "C:/Users/10270/Desktop/py_test/test_10_4.py", line 28, in test
    raise StopIteration
StopIteration

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "C:/Users/10270/Desktop/py_test/test_10_4.py", line 35, in <module>
    next(f)
RuntimeError: generator raised StopIteration

You can see the throwing exception. Why did I catch the exception? Why didn't I catch it??? It's because I raise StopIteration. If I change it to return, it won't be like this. I don't know why? Maybe I'm abnormal or didn't learn well. Is it because I threw an exception myself, and then threw it again when I didn't get the value next? incomprehension? Does the great God understand? Help answer the following questions. Thank you very much!

Posted by LMarie on Mon, 04 Oct 2021 15:26:44 -0700