Yield - Ruby vs. Python

Posted by Alex Engelhart on January 12, 2020

(Note: This post represents my current understanding of the topic as a student, and should not be solely relied on for anything important. If anything here is inaccurate, please let me know and I’ll correct the content here.)

At first glance, it might seem that the yield keyword is basically the same between Ruby and Python, since they are both used in iteration. However, there are some key differences that relate to how the two languages actually handle iteration and interaction between functions. The way I think of it is that Ruby yields to, Python yields up.

Ruby yields to a block

In Ruby, when you use the yield keyword in a method, what you’re saying is that you are expecting there to be a block passed to the method - in other words, when someone calls your method, they should be following it with do. You use yield just like a method call - for instance, you could place new_value = yield(old_value) in your method to indicate that old_value should be passed as an argument to a block, then the return value of that block stored as new_value.

For example, I could make something like this:

class Song
  attr_accessor :name, :artist, :genre
  @@all = []
  # other code here

  def self.print_review_scores
    all.each do |song|
      score = yield(song)
      puts "#{song.name} has a review score of #{score}."
    end
  end
end

What this means is that I am passing self as an argument to whatever block gets passed to #print_review_score. Imagine, then that I had a method that assigned review scores like this:

def snobby_review(song)
  song.genre.name == 'classical' ? 10 : 1
end

I could then make this method:

def print_snobby_reviews
  Song.print_review_scores do |song|
	  snobby_review(song)
  end
end

…and because yield hands control over to whatever is in the passed-in block, it would work exactly the same as if I had written my original method as:

def self.print_review_scores
  all.each do |song|
    score = snobby_review(song)
    puts "#{song.name} has a review score of #{score}."
  end
end

Python yields up a value

The same class listed above would be written in Python as follows:

class Song:
    all = []
    # __init__ and getters/setters go here
    @classmethod
		def print_review_scores(cls, revew_generator):
        for review in review_generator(cls.all):
            print(f"{review['songname']} has a review score of {review['score']}")

So where’s yield? Well, in Python, yield works a lot more like a return statement, except that rather than returning a single value, it returns the next item in whatever iteration it happens to be running. Thus, the methods above would be implemented like this:

def snobby_review(song):
    if song.genre.name == "classical":
		    return 10
		else:
		    return 0
				
def generate_snobby_reviews(songs):
    for song in songs:
		    yield snobby_review(song)

Thus, each time generate_snobby_reviews is called in print_review_scores, it returns the next value in the array that was passed to it.