{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "\"Market\n", "\n", "# Supermarket Story\n", "\n", "Lets think about a Market simulation wherein the Inventory is a separate object. \n", "\n", "Distilling inventory into its own type (Inventory class) suggests how we could later distill other functions within a store. Suppose we need to change those prices. Perhaps we'll need a customer service desk the sells postage stamps. Our model could become much more intricate and detailed.\n", "\n", "We might have our cost centers or profit centers, depending on circumstance and terminology. If we have a chain of stores, they might all be reading from and writing to a centralized database, which brings up glossary topics such as \"ACID compliance\".\n", "\n", "Lets leave all that to the imagination and concentrate just on an Inventory. While learning Python, we have no need to clutter the vista with unnecessary complexity." ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "class Inventory:\n", " \"\"\"\n", " Supermarket brings an Inventory instance on board upon\n", " initialization, increments / decrements items, reads\n", " and writes to json file. Does not track cash.\n", " \"\"\"\n", " \n", " def __init__(self, the_file):\n", " self.storage = the_file\n", " with open(the_file, 'r') as warehouse:\n", " self.wares = json.load(warehouse)\n", " \n", " def save_items(self):\n", " with open(self.storage, 'w') as warehouse: \n", " json.dump(self.wares, warehouse, indent=4)\n", " \n", " def remove_item(self, item, qty):\n", " if qty > self.wares[item][1]:\n", " raise OutOfStock\n", " self.wares[item][1] -= qty\n", " \n", " def add_item(self, item, qty):\n", " self.wares[item][1] += qty\n", " \n", " def __str__(self): # enhancement since version 2\n", " line = \"\"\n", " for idx, item in enumerate(self.wares.items(), start=1):\n", " line = line + \"\\n\" + \\\n", " \"{:3}: {}\\n\".format(idx, item[0]) + \\\n", " \" Price: {}\\n\".format(item[1][0]) + \\\n", " \" Qty : {}\\n\".format(item[1][1])\n", " return line\n", " \n", " def __repr__(self):\n", " return \"Inventory('{}')\" % self.storage\n", "\n", "def test_data():\n", " \"\"\"\n", " 10 of everything keeps it simple, however \n", " feel free to take over this section. Add\n", " whatever products needed for the simulations.\n", " \"\"\"\n", " stuff = {\n", " \"Snicker-Snacks\": [5.99, 10],\n", " \"Polly's Peanuts\": [3.99, 10],\n", " \"Dr. Soap\": [4.99, 10]}\n", " with open(\"the_stuff.json\", 'w') as warehouse: \n", " json.dump(stuff, warehouse, indent=4)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We're given some test data, for convenience, consisting of current price and quantity on hand. Typically we maintain a testing version of our product, and a production version. The latter will have to grapple with a more current and complete set of data.\n", "\n", "On creation (initialization), a Supermarket type object (below) will instantiate a corresponding Inventory (above) and use ```wares``` as an active dictionary whereby \"quantities on hand\" might be changed.\n", "\n", "No provision is made for changing prices, e.g. 5.99 in the case of Snicker-Snacks. Those would be methods a developer might add for the next version. \n", "\n", "We could imagine a Clerk type with the job of changing price tags on items. We could also imagine an Ordering department, and Payroll. \n", "\n", "Lets keep leaving all that to the imagination, as our focus here is more the grammar of Python than the guts of a fully functional SuperMarket.\n", "\n", "Here's a Supermarket type, with some customized Exceptions it might be concerned with:" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "import json\n", "\n", "class NoMoney(Exception):\n", " pass\n", "\n", "class OutOfStock(Exception):\n", " pass\n", "\n", "class SuperMarket:\n", " \"\"\"\n", " Persists buyable items in a json file.\n", " Initializes with 0 cash\n", " \"\"\"\n", " \n", " def __init__(self):\n", " self.inventory = Inventory(\"the_stuff.json\")\n", " # self.clerk = Clerk() <- suggestive\n", " self.cash = 0 # not persisted through inventory\n", " # in a future version, clerk might handle cash\n", " \n", " def buy(self, shopper, item, how_many):\n", " \"\"\"\n", " remove money from shopper wallet, add qty of item\n", " to basket, abort if customer short on cash\n", " \"\"\"\n", " if item in self.inventory.wares: # check keys\n", " price = self.inventory.wares[item][0]\n", " try:\n", " # might have to undo\n", " self.inventory.remove_item(item, how_many)\n", " shopper.add_item(item, price, how_many)\n", " except NoMoney:\n", " # undo removing item from inventory:\n", " # put the item back if unable to purchase\n", " # this is the bug fix <------------------\n", " # since version 2.\n", " # print(\"Restoring item\")\n", " self.inventory.add_item(item, how_many)\n", " raise # re-raise exception\n", " except OutOfStock:\n", " # print(\"Don't have enough in stock\")\n", " raise\n", " else:\n", " # we get this far only if no exceptions\n", " self.cash += round(price * how_many, 2)\n", "\n", " def close(self):\n", " \"\"\"\n", " write json file\n", " \"\"\"\n", " self.inventory.save_items()\n", " \n", " def __repr__(self):\n", " return \"SuperMarket with cash: {}\".format(self.cash)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Clearly the ```buy``` method is the focus of this type. The initializer merely reads in a JSON file and turns that in to a Python dictionary, ```self.wares```. \n", "\n", "A buy (a transaction) features one shopper, an item to buy (subtracted from ```self.inventory.wares```, add to ```shopper.basket```), and a how many (units, integer number or quantity). Money also changes hands, decrementing ```shopper.wallet``` and incrementing ```self.cash```. \n", "\n", "Items might be sold by weight and floating point quantities might be imagined. We stick to integers here. We also allow floating point for monetary transactions, whereas ```decimal.Decimal``` might be used.\n", "\n", "During a buy event, two exceptions might be expected: \n", "\n", " * the requested quantity is not in stock (OutOfStock exception)\n", " * the shopper has insuffient funds to cover the purchase (NoMoney exception)\n", "\n", "Finally, it's time for our Shopper type:" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [], "source": [ "class Shopper:\n", " \n", " def __init__(self, name, budget):\n", " self.name = name\n", " self.basket = { }\n", " self.wallet = budget # budgeted allowance\n", "\n", " def add_item(self, item, price, qty):\n", " \"\"\"\n", " add qty of item to basket and pay, if money available\n", " we shouldn't get here unless the quantity was on hand,\n", " per how Supermarket.buy works.\n", " \"\"\"\n", " if self.wallet - qty * price < 0:\n", " # the calling buy method will need to put the \n", " # item back into inventory, not a feature in\n", " # version 2 i.e. this is a bug fix.\n", " raise NoMoney\n", " \n", " # Pythonic or too tricky? We debated on edu-sig\n", " # I'd say idiomatic among some Pythonistas\n", " # if initializing item, there's nothing to get yet\n", " # so default to 0 and add incoming quantity\n", " self.basket[item] = self.basket.get(item, 0) + qty\n", " # should work fine because we've already checked\n", " # ahead at the self.wallet amount for affordability\n", " self.wallet -= qty * price\n", " \n", " def __repr__(self):\n", " return \"Shopper {}:\\n Wallet {};\\n Basket {}\".format(\n", " self.name, self.wallet, self.basket)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now we're ready for a simulation. The simulator has to know what to do in case exceptions get raised." ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [], "source": [ "def simulation():\n", " test_data() # initialize the_stuff.json\n", " kirby = Shopper(\"Kirby\", 1000)\n", " market = SuperMarket()\n", "\n", " print(\"------- Buy Event 1 -------\")\n", "\n", " try:\n", " market.buy(kirby, \"Snicker-Snacks\", 2)\n", " market.buy(kirby, \"Polly's Peanuts\", 2)\n", " market.buy(kirby, \"Dr. Soap\", 100) # triggers exception\n", " except NoMoney:\n", " pass\n", " print(\"Uh oh, out of money!\")\n", " except OutOfStock:\n", " pass\n", " print(\"Uh oh, out of stock!\")\n", "\n", " print(kirby)\n", " print(\"Inventory on hand:\", market.inventory)\n", " \n", " print(\"------- Buy Event 2 -------\")\n", " # kirby didn't get away with buying that much Dr. Soap, but \n", " # the first two transactions got through. Undetered, kirby\n", " # tries again to buy the soap. We expect all the soap to be \n", " # gone, with 8 left of the other two items. We had 10 of \n", " # everything to start.\n", " \n", " try:\n", " market.buy(kirby, \"Dr. Soap\", 10) # should be OK this time\n", " except NoMoney:\n", " pass\n", " print(\"Uh oh, out of money!\")\n", " except OutOfStock:\n", " pass\n", " print(\"Uh oh, out of stock!\")\n", " \n", " print(kirby)\n", " print(\"Inventory on hand:\", market.inventory)\n", " \n", " market.close()\n", " print(market)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The simulation takes a Shopper through two \"buying sprees\". We're not sure which will buy will abort, if any. We could allow only one buy at a time. \n", "\n", "The Dr. Soap transaction, asking for 1000, does not go through, but then the customer comes back and shops for 10 of that same item, and this should work. \n", "\n", "Lets see..." ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "------- Buy Event 1 -------\n", "Uh oh, out of stock!\n", "Shopper Kirby:\n", " Wallet 980.04;\n", " Basket {'Snicker-Snacks': 2, \"Polly's Peanuts\": 2}\n", "Inventory on hand: \n", " 1: Snicker-Snacks\n", " Price: 5.99\n", " Qty : 8\n", "\n", " 2: Polly's Peanuts\n", " Price: 3.99\n", " Qty : 8\n", "\n", " 3: Dr. Soap\n", " Price: 4.99\n", " Qty : 10\n", "\n", "------- Buy Event 2 -------\n", "Shopper Kirby:\n", " Wallet 930.14;\n", " Basket {'Snicker-Snacks': 2, \"Polly's Peanuts\": 2, 'Dr. Soap': 10}\n", "Inventory on hand: \n", " 1: Snicker-Snacks\n", " Price: 5.99\n", " Qty : 8\n", "\n", " 2: Polly's Peanuts\n", " Price: 3.99\n", " Qty : 8\n", "\n", " 3: Dr. Soap\n", " Price: 4.99\n", " Qty : 0\n", "\n", "SuperMarket with cash: 69.86\n" ] } ], "source": [ "simulation() # finally, lets DO something..." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Does the shopper's original allowance, minus what's in the Supermarket's cash account, match what's currently in the shopper's wallet?" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "930.14" ] }, "execution_count": 6, "metadata": {}, "output_type": "execute_result" } ], "source": [ "1000 - 69.86 " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Tests such as the above might be gathered up as unittests. See if you have ```test_shopping.py```, which is designed to be matched with shopping_v2.py (in which case a test will fail) as well as shopping_v3.py (which should pass all the tests)." ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.7.4" } }, "nbformat": 4, "nbformat_minor": 2 }