Building better programs in Python (Part 2)

Example: making more sandwiches

Jam and peanut butter sandwiches.
Image: ericro/iStock/Thinkstock

Going back to our sandwich spread example, what if we wanted to figure out how many sandwiches we could make, assuming that a bread of particular width and length is used, from given numbers of jars of jam and peanut butter? We assume that a uniform layer of jam and butter is applied and that the thickness of the layer depends on whether it is made of jam or butter. The steps involved in writing the function is as follows:

  • Compute the number of sandwiches worth of jam.
  • Compute the number of sandwiches worth of peanut butter.
  • Compute the total number of sandwiches.

The first two steps look like the same kind of computation. This looks like a job for a helper function. We will write a helper function by extending the function we wrote earlier by including thickness and jar volume as parameters.


Writing the code

import math
def sandwiches(width, length, number_jars,
               thickness, jar_volume):

To handle the first two steps, we write a helper function by extending the function we wrote earlier by including the thickness and jar volume as parameters.


    """Determines the number of sandwiches 
       that can be made

       Preconditions: 
       width: int or float with value > 0
       length: int or float with value > 0
       number_jars: int with value >= 0
       thickness: int or float with value > 0
       jar_volume: int or float with value > 0

       Parameters:
       width: the width of a piece of bread in cm
       length: the length of a piece of bread in cm
       number_jars: the number of jars of spread
       thickness: the thickness of the spread in cm
       jar_volume: volume of each jar in cubic cm

       Returns: int number of sandwiches
    """   

We need to update the docstring to include the new parameters.


    ## Compute volume for one sandwich
    volume_for_one = width * length * thickness

Here the parameter thickness plays the role of the constant thickness in our earlier function.


    ## Compute total volume of jar contents
    volume_available = number_jars * jar_volume

Here the parameter jar_volume plays the role of the constant jar_volume in our earlier function.


    ## Compute total sandwiches
    return math.floor( \
           volume_available / volume_for_one)

def pbj(width, length, number_jam_jars, jam_thickness, 
        jam_jar_volume, number_pb_jars, pb_thickness,
        pb_jar_volume):

We start the main function with its header.


    """Determines the number of sandwiches 
       that can be made.

Here we're going to opt to keep the summary short, leaving the details to explain all of the many parameters.


       Preconditions: 
       width: int or float with value > 0
       length: int or float with value > 0
       number_jam_jars: int with value >= 0
       jam_thickness: int or float with value > 0
       jam_jar_volume: int or float with value > 0
       number_pb_jars: int with value >= 0
       pb_thickness: int or float with value > 0
       pb_jar_volume: int or float with value > 0

We ensure that width, length, thicknesses, and volumes are all nonnegative and that numbers of jars are nonnegative integers.


       Parameters:
       width: width of a piece of bread in cm
       length: length of a piece of bread in cm
       number_jam_jars: number of jars of jam
       jam_thickness: thickness of jam in cm
       jar_volume: volume of jam jar in cubic cm
       number_pb_jars: number of jars of pb
       pb_thickness: thickness of pb in cm
       pb_volume: volume of pb jar in cubic cm

The explanations of the parameters specify that measures are in centimeters, that width and length refer to dimensions of bread, and that for each of the two spreads, namely jam and peanut butter, we give the number of jars, the thickness of the spread required, and the volume of each jar.


       Returns: int number of sandwiches
    """

We return an integer number of sandwiches.


    ## Compute number of sandwiches worth of jam
    number_jam = sandwiches(width, length, \
                 number_jam_jars, jam_thickness, \
                 jam_jar_volume)

We use our helper function sandwiches to determine the number of jam sandwiches we can make, storing the result in the variable number_jam.


    ## Compute number of sandwiches worth of pb
    number_pb = sandwiches(width, length, \
                number_pb_jars, pb_thickness, \
                pb_jar_volume)

We use the helper function again to determine the number of peanut butter sandwiches we can make, storing the result in the variable number_pb.


    ## Return minimum of values
    return min(number_jam, number_pb)

To complete the last step of the function, we return the minimum of the two numbers of sandwiches.


Here's our function when all parts are put together:

import math
def sandwiches(width, length, number_jars,
               thickness, jar_volume):
    """Determines the number of sandwiches 
       that can be made

       Preconditions: 
       width: int or float with value > 0
       length: int or float with value > 0
       number_jars: int with value >= 0
       thickness: int or float with value > 0
       jar_volume: int or float with value > 0

       Parameters:
       width: the width of a piece of bread in cm
       length: the length of a piece of bread in cm
       number_jars: the number of jars of spread
       thickness: the thickness of the spread in cm
       jar_volume: volume of each jar in cubic cm

       Returns: int number of sandwiches
    """   
    ## Compute volume for one sandwich
    volume_for_one = width * length * thickness

    ## Compute total volume of jar contents
    volume_available = number_jars * jar_volume 

    ## Compute total sandwiches
    return math.floor( \
           volume_available / volume_for_one)

def pbj(width, length, number_jam_jars, jam_thickness, 
        jam_jar_volume, number_pb_jars, pb_thickness,
        pb_jar_volume):
    """Determines the number of sandwiches 
       that can be made.

       Preconditions: 
       width: int or float with value > 0
       length: int or float with value > 0
       number_jam_jars: int with value >= 0
       jam_thickness: int or float with value > 0
       jam_jar_volume: int or float with value > 0
       number_pb_jars: int with value >= 0
       pb_thickness: int or float with value > 0
       pb_jar_volume: int or float with value > 0

       Parameters:
       width: width of a piece of bread in cm
       length: length of a piece of bread in cm
       number_jam_jars: number of jars of jam
       jam_thickness: thickness of jam in cm
       jar_volume: volume of jam jar in cubic cm
       number_pb_jars: number of jars of pb
       pb_thickness: thickness of pb in cm
       pb_volume: volume of pb jar in cubic cm
 
       Returns: int number of sandwiches
    """   

    ## Compute number of sandwiches worth of jam
    number_jam = sandwiches(width, length, \
                 number_jam_jars, jam_thickness, \
                 jam_jar_volume)

    ## Compute number of sandwiches worth of pb
    number_pb = sandwiches(width, length, \
                number_pb_jars, pb_thickness, \
                pb_jar_volume)

    ## Return minimum of values
    return min(number_jam, number_pb)

Example: frosting a wedding cake

A cake with 5 layers.
Image: tiero/iStock/Thinkstock

As our last example, suppose we wish to compute the amount of frosting needed to frost a wedding cake. Assume that the cake consists of three stacked concentric circular discs with decreasing radius. The larger discs are placed lower down in the stack. For each of the discs, frosting is applied to the top of the disc and its sides. Given the radius and height of each of the disks and the uniform thickness of frosting needed, our task is to calculate the total amount of frosting needed for the cake.

We make use of three helper functions, area_circle, volume_cylinder, and round_cake_spread before writing our main function, wedding_cake. The first and the second helper functions compute the area of a circle and volume of a cylinder, respectively, and they are used by the third helper function. The third helper function computes frosting needed to frost a single-layer wedding cake. The steps needed for the helper function round_cake_spread is as follows:

  • Calculate frosting around the side.
  • Calculate frosting on top of layer, including sides.
  • Calculate the total frosting.

The steps needed for the main function, wedding_cake, is repeatedly applying the helper function round_cake_spread to each of the three layers and summing up the results.

import math

def area_circle(radius):
    return math.pi * radius ** 2

We use a helper function that returns the area of a circle. Since we are using π, we make sure to import math. This helper functions should also have docstrings, but we've left them out for the sake of space and readability.


def volume_cylinder(radius, height):
    return height * area_circle(radius)

We use a helper function that returns the volume of a cylinder.


def round_cake_spread(radius, height, thickness):

Since all three layers will need to be computed in the same way, we create a helper function. The parameters are for a single layer.


    """Determines volume of frosting of given 
       thickness for a round cake of given 
       radius and height.


       Preconditions: 
       radius: int, float; value > 0
       height: int, float; with value > 0
       thickness: int, float; with value > 0

It is important here that the preconditions match those in the other function, since we'll be passing along the values.


       Parameters:
       radius: radius of cake, in cm
       height: height of cake, in cm
       thickness: thickness of frosting, in cm

       Returns: float volume of frosting
    """

Not surprisingly, the rest of the docstring looks similar as well.


    ## Frosting around side
    thick_radius = radius + thickness

The frosting around the side of a cake is like the volume of a ring, that is the difference of two cylinders. So, to determine the amount of frosting on the side of a layer, we determine the difference of two cylinders. The radius of the outer cylinder is the radius of the layer plus the thickness of the frosting.


    outer = volume_cylinder(thick_radius, height)
    inner = volume_cylinder(radius, height)

We use outer to store the volume of the outer cylinder and inner to store the volume of the inner cylinder, which is the layer of cake itself. Both use the helper function volume-cylinder.


    side_frosting = outer - inner

The total amount of frosting for the side of the layer is the difference of these quantities.


    ## Frosting on top of layer, including side
    top_frosting = area_circle(thick_radius)  * thickness

The frosting of the top layer can be obtained by multiplying the area of the top, again using the bigger radius, and the thickness.


    ## Total frosting
    return top_frosting + side_frosting

Finally, we return the total. The value returned is the sum of the amount needed for the side and the top.


def wedding_cake(radius1, radius2, radius3, height1, 
                height2, height3, thickness):

We'll assume we want the same thickness of frosting on all the layers, but that the layers might be of different heights.


    """Determines volume of frosting of given 
       thickness for a wedding cake of three 
       layers of given radius and height.

We create the docstring summary, here mentioning some but not all of the parameters. Here, the summary can refer to all the parameters without getting too long.


       Preconditions: 
       radius1, radius2, radius3: int, float; value > 0
       height1, height3, height3: int, float; value > 0
       thickness: int or float with value > 0

Since some of the parameters serve similar roles, we've grouped them together in the preconditions. All parameters are positive numbers.


       Parameters:
       radius1, radius2, radius3: radius of cake, in cm
       height1, height2, height3: height of cake, in cm
       thickness: thickness of the frosting, in cm

Again, we've grouped similar parameters in explaining the meanings.


       Returns: float volume of frosting
    """

We've chosen to return the volume as a floating point number, though it would have been just as reasonable to choose an integer. Maybe more so, since the quantities used make or buy frosting are most likely integers.


    frosting1 = round_cake_spread(radius1, height1, \
                thickness);

We use the helper function round_cake_spread to determine the amount needed for the first layer.


    frosting2 = round_cake_spread(radius2, height2, \
                thickness);

Again, we use the helper function round_cake_spread to determine the amount needed for the second layer.


    frosting3 = round_cake_spread(radius3, height3, \
                thickness);

Again, we use the helper function round_cake_spread to determine the amount needed for the third layer.


    return frosting1 + frosting2 + frosting3

Finally, we return the sum of the three.


Here's our code when all parts are put together:

import math

def area_circle(radius):
    return math.pi * radius ** 2

def volume_cylinder(radius, height):
    return height * area_circle(radius)

def round_cake_spread(radius, height, thickness):
    """Determines volume of frosting of given 
       thickness for a round cake of given 
       radius and height.

       Preconditions: 
       radius: int, float; value > 0
       height: int, float; with value > 0
       thickness: int, float; with value > 0

       Parameters:
       radius: radius of cake, in cm
       height: height of cake, in cm
       thickness: thickness of frosting, in cm

       Returns: float volume of frosting
    """   
    ## Frosting around side
    thick_radius = radius + thickness
    outer = volume_cylinder(thick_radius, height)
    inner = volume_cylinder(radius, height)
    side_frosting = outer - inner

    ## Frosting on top of layer, including side
    top_frosting = area_circle(thick_radius) \
                   * thickness
    
    ## Total frosting
    return top_frosting + side_frosting

def wedding_cake(radius1, radius2, radius3, height1, 
                height2, height3, thickness):
    """Determines volume of frosting of given 
       thickness for a wedding cake of three 
       layers of given radius and height.

       Preconditions: 
       radius1, radius2, radius3: int, float; value > 0
       height1, height3, height3: int, float; value > 0
       thickness: int or float with value > 0

       Parameters:
       radius1, radius2, radius3: radius of cake, in cm
       height1, height2, height3: height of cake, in cm
       thickness: thickness of the frosting, in cm

       Returns: float volume of frosting
    """   
    frosting1 = round_cake_spread(radius1, height1, \
                thickness);
    frosting2 = round_cake_spread(radius2, height2, \
                thickness);
    frosting3 = round_cake_spread(radius3, height3, \
                thickness);
    return frosting1 + frosting2 + frosting3