Program design: bakery inventory
About this video
The video designs a bakery inventory program using dictionaries for recipes, inventory and per-unit prices. It writes scale_recipe, place_order and get_cost, choosing which dictionary to iterate over, using the get method to default missing inventory to 0, and deciding to let a missing price raise a KeyError.
You'll be able to
- break a real problem into functions that work with dictionaries.
- decide which dictionary to iterate over based on what the function must check.
- use dict.get(key, default) to handle missing keys.
- judge when a KeyError is better than a default value.
Key words
- accumulate
- Build up a new result, like a new dictionary, step by step inside a loop.
- pass by assignment
- Python passes the same object into a function, so mutating a parameter changes the original.
- KeyError
- The error Python raises when you look up a key that isn't in a dictionary.
- get method
- d.get(key, default) returns the value for key, or the default if the key is missing.
Transcript
- [Instructor] Let's design a program with dictionaries. We run a small bakery, which means that we spend a lot of our time just managing our inventory. At the start of every week, we have to plan out which recipes we're going to make, what ingredients we need, and then place orders for those ingredients with our vendors.
At the same time, we have to pay attention to our ingredient costs to make sure the recipes we're choosing make good business sense on top of being delicious. So let's build a program to help do our inventory management. I started by inputting all of the data I have.
First, I represented all of my bakery recipes as dictionaries, where the keys are the ingredients and the values are the amounts. To make it simple, I put all of the amounts in grams, except for items like eggs or bananas, those numbers are just a count. In a separate module, I recorded my inventory.
Right now I'm out of stock of a lot of ingredients. And finally, I estimated the per unit cost of each ingredient. I know I can't technically buy a single gram of flour, but representing my costs this way helps with my weekly accounting. Then I'll create one last module to hold all my functions.
But before we get into the code there, let's jump back to our main module and outline how we want this logic to work. I'll start with a simplified case where we just make one type of recipe a week. We need to compare our recipe against our current inventory to see which ingredients we're missing.
This will tell us how much of each ingredient we need to order. And once we have that order, then we can calculate the cost, which we'll need to take in our prices dictionary. One last thing to consider. This recipe only represents a single batch of cupcakes, which is not going to be enough for an entire week.
So let's add one last function that can take in a recipe and a number of batches, which can then return out the total ingredients needed. Of course, if I run my program now I get a runtime error because I didn't define any of those functions. So let's jump back into the manager module and get to work.
Let's start with the scale_recipe function. Now, because of pass by assignment, we don't wanna mutate this recipe parameter directly. We might need the original version of this recipe later, and if we mutate it inside this function, it'll affect the dictionary we passed in, so instead, let's accumulate a new dictionary.
We scale up an ingredient just by multiplying its amount by the number of batches. So to scale up the whole recipe, we need to iterate over every ingredient. And since we need both the ingredient name and the amount, it's easiest if we iterate by item. We can do a real easy check by testing one batch, and then we should be able to easily verify the result for 10 batches.
This looks good, so let's move on to the place_order function. The place_order function takes in two dictionaries, the current inventory and the recipe, and it returns out a new dictionary with the missing ingredients we need to order. But here's the question, which of these dictionaries do we iterate over?
Well, there might be ingredients in our inventory that aren't in our recipe, like if we're making cupcakes, we don't need the bananas, but there might also be ingredients in our recipe that aren't in our inventory, like we may be out of eggs. For this to work, we need to check every ingredient in our recipe to see if our inventory has enough of it, which means the thing we need to iterate over is our recipe.
On each iteration then, we check how much of that ingredient we have in our inventory. If that amount is greater than or equal to the amount that our recipe needs, then we're fine. But if it's not, then we need to order more, and that amount is just the amount we need minus the amount that we have.
And if we run our program now, we have a KeyError I guess. This stack trace tells us that our inventory dictionary doesn't have the key flour. We might be out of stock of ingredients a lot, so our function should try and handle it. If the key doesn't exist in our dictionary, that means we have zero of that ingredient, and thus should order the full amount required by the recipe.
Sounds like a good time for the get method because instead of raising a key error, we want to default the amount to 0. Get is a method so we call it using dot syntax, and then it takes in the key to look up, followed by the default value. Much better. Onto our last function, get_cost.
It takes in the ingredients in our order and our dictionary of unit prices. This time, instead of returning a dictionary, we wanna return a float, which is the total cost for all the ingredients in the order. And since the number is a price, we'll round it to two decimal places.
Here, it makes the most sense to iterate over the order since those are really the only ingredients we care about. The calculation here is pretty straightforward. We get the price_per_unit for the ingredient, we multiply it by the amount we need, and we add that to the total cost.
But what if the ingredient isn't in the prices dictionary, should we use the get method here too? In this case, it's not immediately clear because what would the default value be? If we make it 0, then we're effectively saying that ingredient is free. The caller would have no idea that we secretly skipped an ingredient.
I might argue that the best option here is to let it raise the key error, because if we're missing the price for an ingredient, it's impossible to correctly calculate the order cost. The only way to fix the program would be for someone to come in and add that data to the price_per_unit dictionary, which the key error at least brings attention to, so let's leave this as a standard dictionary access.
With all our functions written out now, we can run through a few different recipes and compare their ingredient costs. The next step would be to extend this to work across multiple weeks and multiple recipes, where I update the state of my inventory based on the ingredients I've used.
Video © Khan Academy, used under CC BY-NC-SA. Watch on Khan Academy. Puzzicle is not affiliated with Khan Academy.