Need clarification on Generators .send() method

I’m trying to wrap my head around the way .send() methods works in generators. I can memorize the syntax, but I’d rather try to understand what goes on under the hood.

If someone could elaborate this concept to me, I’d highly appreciate it.

Lines 6-8 don’t make sense to me syntactically. The variable days is already assigned and passed to the generator. How come we use None then? I don’t understand how yield days would equate to None. Is there a logic behind this or is this simply a built-in trick?

days = 25 #Countdown generator def graduation_countdown(days): while days > 0: days_left = yield days if days_left != None: days = days_left else: days -= 1 #Countdown generator stored to a variable grad_days = graduation_countdown(days) #Looping through the generator for day in grad_days: if day == 15: grad_days.send(10) elif day == 3: grad_days.close() print("Days Left: " + str(day))

There’s some decent discussion at the following FAQ page that might help-

Whilst days is passed in it’s perfectly valid to reassign that name to something else. What is somewhat unusual about yield is that it can be used as an expression so it evaluates to return a value (in addition to actually yielding a value from the generator). Whatever you .send to the generator object is what the expression evaluates to (within the generator itself) so it’s the object you .send into the generator that will be assigned to days.

If you’ve used next(gen_obj) to advance an iterator that’s basically the same as .send(None). So name = yield obj can and often does evaluate to None. If you’re using a mixture of next(gen_obj) and gen_obj.send(obj) calls then handling None is necessary.

def gen(): for x in range(100): print((yield x)) gen_obj = gen() next(gen_obj) # the first one does nothing gen_obj.send(None) # basically the same as next gen_obj.send("Boo") # actually send something gen_obj.send("Boo") # actually send something

Under the hood is a bit trickier as you’d need to know several other concepts beforehand that might not be worth the hassle at this point.

If you’re curious there’s a rough description below but it’ll likely take time, effort and reading a much better resource until it makes sense.

Execution frames and generator objects

The first thing to understand would be how frames work in Python (this would be CPython implementation rather than the Python language itself); the FAQ has a link out to a visualisation tool which can help a lot. Then you’d want to dig into the basic of bytecode (instructions to run). Frames in Python are accessible as objects that hold data, a set of instructions (the bytecode) and the current instruction (position) to be executed along with a few other things. Each new function call (excluding built-ins and C based functions) pauses the current frame, creates and populates (with data) a new frame and starts running it instead. Once the function finishes its execution its frame is discarded and the previous frame (from which that function call was made) continues its execution from the point it stopped.

A generator object is basically a frame that’s kept alive for as long as needed. So each time you call next the current frame is temporarily paused, the generator’s frame is run until it reaches another yield and the yielded object is passed back from this generator frame to the frame from which next was called. Unlike a normal function call the frame is not discarded (unless the generator hits return, runs off the end of its instructions or is deliberately closed or cleared from memory).

When you .send you are passing objects into the existing frame which it can then work with. Whatever you pass in is what the yield evaluates to within the generator and it is then up to the bytecode instructions in the generator itself to decide what to do with it.

I though I understood what I did not understand, but now I don’t understand even that :slight_smile:

Why does my last line here push the next student instead of sending the one I’ve put in the send method?

#Version 1.1 guests = {} def read_guestlist(file_name): #Open file text_file = open(file_name,'r') #Read text file by line while True: line_data = text_file.readline().strip().split(",") # If no more lines, close file if len(line_data) < 2: text_file.close() break #Save Name and Age to a Dict on each iteration name = line_data[0] age = int(line_data[1]) student = name, age back = yield student if back != None: student = back read = read_guestlist("guest_list.txt") # for num in range(10): print(next(read)) print(read.send("Jane, 35"))

P.S.: Thank you for your time.

Haha Python abstracts a lot so trying to dig into it can be tricky since there’s layer upon layer of abstraction, under the hood is scary :laughing:. I think understanding basic generators is of more worth than .send; they’re useful everywhere whereas .send is more common for callbacks and certain asynchronous tasks. If you need it you’ll likely run into it again.

I think you might have problems here in that you are always going to read a line from your file regardless of whether you send a value in or not. Check the logical flow from what happens if something gets sent in and I think you’ll see that the value sent in is basically overwritten by the file read. You’d need to adapt your code to prevent the file being read when something is sent in.

If you’re struggling to follow the logic using print or the visualisation tool linked in that FAQ post would likely be helpful.

Added a link to the actual problem since there’s no way to run the code in the codebyte as it relies on a file-
https://www.codecademy.com/courses/learn-intermediate-python-3/lessons/int-python-generators/exercises/generator-methods