'Creating a new method

I'm working on a MOOC on Python Programming and am having a hard time finding a solution to a problem set. I hope you can provide some assistance.

The problem is: The Fibonacci sequence is a number sequence where each number is the sum of the previous two numbers. The first two numbers are defined as 0 and 1, so the third number is 1 (0 + 1 = 1), the fourth number is 2 (1 + 1 = 2), the fifth number is 3 (1 + 2 = 3), the sixth number is 5(2 + 3 = 5), and so on.

Below we've started a class called FibSeq. At any time, FibSeq holds two values from the Fibonacci sequence: back1 and back2.

Create a new method inside FibSeq called next_number. The next_number method should:

  • Calculate and return the next number in the sequence, based on the previous 2.
  • Update back2 with the former value of back1, and update back1 with the new next item in the sequence.

This means that consecutive calls to next_number should yield each consecutive number from the Fibonacci sequence. Calling next_number 5 times would print 1, 2, 3, 5, and 8.

My code is below:

class FibSeq:
    def __init__(self):
        self.back1 = 1
        self.back2 = 0
    
    def next_number(self):
        self.back1 = self.back1 + self.back2
        self.back2 = self.back1 - self.back2
        yield(self.back1)

f = FibSeq()
for i in range(5):
    s = f.next_number()
    print(next(s))

My code returns the following:


1
2
3
5
8
<generator object FibSeq.next_number at 0x7f68f6fbe678>
<generator object FibSeq.next_number at 0x7f68f6fbe678>
<generator object FibSeq.next_number at 0x7f68f6fbe678>
<generator object FibSeq.next_number at 0x7f68f6fbe678>
<generator object FibSeq.next_number at 0x7f68f6fbe678>

However, it should only return 1,2,3,5,8, after running the below code:

newFib = FibSeq()
print(newFib.next_number())
print(newFib.next_number())
print(newFib.next_number())
print(newFib.next_number())
print(newFib.next_number())

Why does my code return the last 5 "error" statements like this <generator object FibSeq.next_number at 0x7f68f6fbe678>?

Thank you.



Solution 1:[1]

Although I agree that a generator is a much more pythonic solution to this problem, I don't believe the assignment wants you to write a generator. What it says is:

This means that consecutive calls to next_number should yield each consecutive number from the Fibonacci sequence.

Their use of the word "yield" (rather than the more natural "return") is confusing, to be sure, but "return" is surely what they meant because each call to a generator function returns an iterator, not a value, and you need to call next on the returned iterator to get successive values.

Generators and class instances are two different ways of solving the same problem, which is how to retain state while producing successive values of a sequence. In the object-oriented solution, you create an object by calling the class' constructor, and then you repeatedly call an instance method (such as next_number. That would lead to a solution like this:

class FibSeq:
    def __init__(self):
        self.back1, self.back2 = 1, 0
    def next_number(self):
        self.back1, self.back2 = self.back1 + self.back2, self.back1
        return self.back1

# Example usage:
fibber1 = FibSeq()
for i in range(1, 6):
    print(i, fibber1.next_number())

That prints

1 1
2 2
3 3
4 5
5 8

(Note that Fib(0) is also 1. To my mind, a Fibonacci generator should start at 0, but to conform to the expectation that the first five calls to next_number, I started the counter at 1.)

If we want another sequence starting at the beginning, we just create another instance of the class. We can then use both instances independently. Here I print out the next five values produced by the first object, whose next value produced will be Fib(6), and in another column, I output the first five values produced by a new object:

fibber2 = FibSeq()
for i in range(6, 11):
    print(i, fibber2.next_number(), fibber1.next_number())

That prints

6 1 13
7 2 21
8 3 34
9 5 55
10 8 89

And it's easy to see that the two instances of FibSeq are each using their own state (that is, the members back1 and back2).

But that's not very Pythonic (in my opinion). In Python, we can create a generator function to do the same thing; the generator's state is now contained in local variables, not so easily available for inspection. Calling the generator function returns an iterator --a different one each time, with its own local variables-- and the iterators can be used in a for statement or a comprehension. Generators are distinguished from ordinary functions by the fact that they include at least one yield statement. They normally don't include a return statement, or if they do, it doesn't return anything. This has nothing to do with the return value of calling the generator; as I said, calling the generator returns an iterator, but that's done automatically as soon as you call the generator, before any statement has been executed.

So here's the generator version of the Fibonacci sequence:

def FibGen():
    back1, back2 = 1, 0
    while True:
        back1, back2 = back1 + back2, back1
        yield back1

Now, we can use a simple for loop, but with a bit of caution because the iterator produced by FibGen never stops. To specify the desired number of values generated, I can zip it with a range iterator:

for i, f in zip(range(1, 6), FibGen()):
    print(i, f)
# Output:
1 1
2 2
3 3
4 5
5 8

This works because zip stops iterating as soon as one of its argument iterators is done.

In that for loop, I created the iterator without saving it. But I might want to save it, in order to do something like the side-by-side display from the O-O example. That works in a very similar fashion:

# Create one iterator and print the first five values
fibber1 = FibGen()
for i, f in zip(range(1, 6), fibber1):
    print(i, f)

# Now create another, independent iterator:
fibber2 = FibGen()
for i, f1, f2 in zip(range(6, 11), fibber1, fibber2):
    print(i, f1, f2)

The output from the two loops:

1 1
2 2
3 3
4 5
5 8

6 1 13
7 2 21
8 3 34
9 5 55
10 8 89

I can also use the generator to fill in a list comprehension, although again I need to be careful to stop the generation, since I have enough memory to store an infinite list. Again, zipping with a range is a simple way to control the generation count:

v = [f for i,f in zip(range(10), FibGen())]
print(v)
# Output:
[1, 2, 3, 5, 8, 13, 21, 34, 55, 89]

Collecting generated values is obviously simpler for iterators which only generate a finite number of values. But on the whole, you should avoid collecting generated values. It's surprising how rarely you actually need to save the list.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 rici