{ "cells": [ { "cell_type": "markdown", "id": "1331faa1", "metadata": {}, "source": [ "You can order print and ebook versions of *Think Python 3e* from\n", "[Bookshop.org](https://bookshop.org/a/98697/9781098155438) and\n", "[Amazon](https://www.amazon.com/_/dp/1098155432?smid=ATVPDKIKX0DER&_encoding=UTF8&tag=oreilly20-20&_encoding=UTF8&tag=greenteapre01-20&linkCode=ur2&linkId=e2a529f94920295d27ec8a06e757dc7c&camp=1789&creative=9325)." ] }, { "cell_type": "code", "execution_count": 1, "id": "217fc9bf", "metadata": { "tags": [] }, "outputs": [], "source": [ "from os.path import basename, exists\n", "\n", "def download(url):\n", " \"\"\"Download a file if it isn't already here, and return its filename.\"\"\"\n", " filename = basename(url)\n", " if not exists(filename):\n", " from urllib.request import urlretrieve\n", "\n", " local, _ = urlretrieve(url, filename)\n", " print(\"Downloaded \" + str(local))\n", " return filename\n", "\n", "download('https://github.com/porttack/working-in-python/raw/v3/working_in_python.py');\n", "download('https://github.com/AllenDowney/ThinkPython/raw/v3/diagram.py');\n", "\n", "import working_in_python\n", "# apcsp:begin type=\"note\" chapter=\"17\"\n", "working_in_python.enable_docstring_reminders()\n", "# apcsp:end" ] }, { "cell_type": "markdown", "id": "ced31782", "metadata": { "tags": [] }, "source": [ "# Inheritance\n", "\n", "The language feature most often associated with object-oriented programming is **inheritance**.\n", "Inheritance is the ability to define a new class that is a modified version of an existing class.\n", "In this chapter I demonstrate inheritance using classes that represent playing cards, decks of cards, and poker hands.\n", "If you don't play poker, don't worry -- I'll tell you what you need to know." ] }, { "cell_type": "markdown", "id": "b19c4dae", "metadata": {}, "source": [ "## Representing cards\n", "\n", "There are 52 playing cards in a standard deck -- each of them belongs to one of four suits and one of thirteen ranks. \n", "The suits are Spades, Hearts, Diamonds, and Clubs.\n", "The ranks are Ace, 2, 3, 4, 5, 6, 7, 8, 9, 10, Jack, Queen, and King.\n", "Depending on which game you are playing, an Ace can be higher than King or lower than 2.\n", "\n", "If we want to define a new object to represent a playing card, it is obvious what the attributes should be: `rank` and `suit`.\n", "It is less obvious what type the attributes should be.\n", "One possibility is to use strings like `'Spade'` for suits and `'Queen'` for ranks.\n", "A problem with this implementation is that it would not be easy to compare cards to see which had a higher rank or suit.\n", "\n", "An alternative is to use integers to **encode** the ranks and suits.\n", "In this context, \"encode\" means that we are going to define a mapping between numbers and suits, or between numbers and ranks.\n", "This kind of encoding is not meant to be a secret (that would be \"encryption\")." ] }, { "cell_type": "markdown", "id": "a9bafecf", "metadata": {}, "source": [ "For example, this table shows the suits and the corresponding integer codes:\n", "\n", "\n", "| Suit | Code |\n", "| --- | --- |\n", "| Spades | 3 |\n", "| Hearts | 2 |\n", "| Diamonds | 1 |\n", "| Clubs | 0 |\n", "\n", "With this encoding, we can compare suits by comparing their codes." ] }, { "cell_type": "markdown", "id": "a1b46b1a", "metadata": {}, "source": [ "To encode the ranks, we'll use the integer `2` to represent the rank `2`, `3` to represent `3`, and so on up to `10`.\n", "The following table shows the codes for the face cards.\n", "\n", " \n", "| Rank | Code |\n", "| --- | --- |\n", "| Jack | 11 |\n", "| Queen | 12 |\n", "| King | 13 |\n", "\n", "And we can use either `1` or `14` to represent an Ace, depending on whether we want it to be considered lower or higher than the other ranks.\n", "\n", "To represent these encodings, we will use two lists of strings, one with the names of the suits and the other with the names of the ranks.\n", "\n", "Here's a definition for a class that represents a playing card, with these lists of strings as **class variables**, which are variables defined inside a class definition, but not inside a method." ] }, { "cell_type": "code", "execution_count": 2, "id": "ef26adf0", "metadata": {}, "outputs": [], "source": [ "class Card:\n", " \"\"\"Represents a standard playing card.\"\"\"\n", "\n", " suit_names = ['Clubs', 'Diamonds', 'Hearts', 'Spades']\n", " rank_names = [None, 'Ace', '2', '3', '4', '5', '6', '7', \n", " '8', '9', '10', 'Jack', 'Queen', 'King', 'Ace']" ] }, { "cell_type": "markdown", "id": "d63f798a", "metadata": {}, "source": [ "The first element of `rank_names` is `None` because there is no card with rank zero. By including `None` as a place-keeper, we get a list with the nice property that the index `2` maps to the string `'2'`, and so on.\n", "\n", "Class variables are associated with the class, rather than an instance of the class, so we can access them like this." ] }, { "cell_type": "code", "execution_count": 3, "id": "4e4bd268", "metadata": {}, "outputs": [], "source": [ "Card.suit_names" ] }, { "cell_type": "markdown", "id": "c837fff6", "metadata": {}, "source": [ "We can use `suit_names` to look up a suit and get the corresponding string." ] }, { "cell_type": "code", "execution_count": 4, "id": "8aec2a6a", "metadata": {}, "outputs": [], "source": [ "Card.suit_names[0]" ] }, { "cell_type": "markdown", "id": "a59d905e", "metadata": {}, "source": [ "And `rank_names` to look up a rank." ] }, { "cell_type": "code", "execution_count": 5, "id": "baf029e9", "metadata": {}, "outputs": [], "source": [ "Card.rank_names[11]" ] }, { "cell_type": "markdown", "id": "50dda19b", "metadata": {}, "source": [ "## Card attributes\n", "\n", "Here's an `__init__` method for the `Card` class -- it takes `suit` and `rank` as parameters and assigns them to attributes with the same names." ] }, { "cell_type": "code", "execution_count": 6, "id": "91320ea3", "metadata": {}, "outputs": [], "source": [ "%%add_method_to Card\n", "\n", " def __init__(self, suit, rank):\n", " self.suit = suit\n", " self.rank = rank" ] }, { "cell_type": "markdown", "id": "31a2782d", "metadata": {}, "source": [ "Now we can create a `Card` object like this." ] }, { "cell_type": "code", "execution_count": 7, "id": "c04bb77e", "metadata": {}, "outputs": [], "source": [ "queen = Card(1, 12)" ] }, { "cell_type": "markdown", "id": "85e5cf5d", "metadata": {}, "source": [ "We can use the new instance to access the attributes." ] }, { "cell_type": "code", "execution_count": 8, "id": "b182e6fa", "metadata": {}, "outputs": [], "source": [ "queen.suit, queen.rank" ] }, { "cell_type": "markdown", "id": "449225d3", "metadata": {}, "source": [ "It is also legal to use the instance to access the class variables." ] }, { "cell_type": "code", "execution_count": 9, "id": "17ce1a51", "metadata": {}, "outputs": [], "source": [ "queen.suit_names" ] }, { "cell_type": "markdown", "id": "97232ffa", "metadata": {}, "source": [ "But if you use the class, it is clearer that they are class variables, not attributes." ] }, { "cell_type": "markdown", "id": "7a0a79ae", "metadata": {}, "source": [ "## Printing cards\n", "\n", "Here's a `__str__` method for `Card` objects." ] }, { "cell_type": "code", "execution_count": 10, "id": "6709b45a", "metadata": {}, "outputs": [], "source": [ "%%add_method_to Card\n", "\n", " def __str__(self):\n", " rank_name = Card.rank_names[self.rank]\n", " suit_name = Card.suit_names[self.suit]\n", " return f'{rank_name} of {suit_name}' " ] }, { "cell_type": "markdown", "id": "d6c51352", "metadata": {}, "source": [ "When we print a `Card`, Python calls the `__str__` method to get a human-readable representation of the card." ] }, { "cell_type": "code", "execution_count": 11, "id": "e7f9304d", "metadata": {}, "outputs": [], "source": [ "print(queen)" ] }, { "cell_type": "markdown", "id": "76044b9e", "metadata": {}, "source": [ "The following is a diagram of the `Card` class object and the Card instance.\n", "`Card` is a class object, so its type is `type`.\n", "`queen` is an instance of `Card`, so its type is `Card`.\n", "To save space, I didn't draw the contents of `suit_names` and `rank_names`." ] }, { "cell_type": "code", "execution_count": 12, "id": "d589ed70", "metadata": { "tags": [] }, "outputs": [], "source": [ "from diagram import Binding, Value, Frame, Stack\n", "\n", "bindings = [Binding(Value(name), draw_value=False)\n", " for name in ['suit_names', 'rank_names']]\n", " \n", "frame1 = Frame(bindings, name='type', dy=-0.5, offsetx=0.77)\n", "binding1 = Binding(Value('Card'), frame1)\n", "\n", "bindings = [Binding(Value(name), Value(value))\n", " for name, value in zip(['suit', 'rank'], [1, 11])]\n", " \n", "frame2 = Frame(bindings, name='Card', dy=-0.3, offsetx=0.33)\n", "binding2 = Binding(Value('queen'), frame2)\n", "\n", "stack = Stack([binding1, binding2], dy=-1.2)" ] }, { "cell_type": "code", "execution_count": 13, "id": "5518455f", "metadata": { "tags": [] }, "outputs": [], "source": [ "from diagram import diagram, Bbox, make_list, adjust\n", "\n", "width, height, x, y = [2.11, 2.14, 0.35, 1.76]\n", "ax = diagram(width, height)\n", "bbox = stack.draw(ax, x, y)\n", "\n", "value = make_list([])\n", "bbox2 = value.draw(ax, x+1.66, y)\n", "\n", "value = make_list([])\n", "bbox3 = value.draw(ax, x+1.66, y-0.5)\n", "\n", "bbox = Bbox.union([bbox, bbox2, bbox3])\n", "#adjust(x, y, bbox)" ] }, { "cell_type": "markdown", "id": "ccb8e41d", "metadata": {}, "source": [ "Every `Card` instance has its own `suit` and `rank` attributes, but there is only one `Card` class object, and only one copy of the class variables `suit_names` and `rank_names`." ] }, { "cell_type": "markdown", "id": "98c6508d", "metadata": {}, "source": [ "## Comparing cards\n", "\n", "Suppose we create a second `Card` object with the same suit and rank." ] }, { "cell_type": "code", "execution_count": 14, "id": "cadb115d", "metadata": {}, "outputs": [], "source": [ "queen2 = Card(1, 12)\n", "print(queen2)" ] }, { "cell_type": "markdown", "id": "3c92779c", "metadata": {}, "source": [ "If we use the `==` operator to compare them, it checks whether `queen` and `queen2` refer to the same object." ] }, { "cell_type": "code", "execution_count": 15, "id": "6a625fde", "metadata": {}, "outputs": [], "source": [ "queen == queen2" ] }, { "cell_type": "markdown", "id": "278d8abe", "metadata": {}, "source": [ "They don't, so it returns `False`.\n", "We can change this behavior by defining the special method `__eq__`." ] }, { "cell_type": "code", "execution_count": 16, "id": "4f394e57", "metadata": {}, "outputs": [], "source": [ "%%add_method_to Card\n", "\n", " def __eq__(self, other):\n", " return self.suit == other.suit and self.rank == other.rank" ] }, { "cell_type": "markdown", "id": "bd66a9d3", "metadata": {}, "source": [ "`__eq__` takes two `Card` objects as parameters and returns `True` if they have the same suit and rank, even if they are not the same object.\n", "In other words, it checks whether they are equivalent, even if they are not identical.\n", "\n", "When we use the `==` operator with `Card` objects, Python calls the `__eq__` method." ] }, { "cell_type": "code", "execution_count": 17, "id": "2c10425b", "metadata": {}, "outputs": [], "source": [ "queen == queen2" ] }, { "cell_type": "markdown", "id": "23d99d3e", "metadata": {}, "source": [ "As a second test, let's create a card with the same suit and a different rank." ] }, { "cell_type": "code", "execution_count": 18, "id": "c2a695b4", "metadata": {}, "outputs": [], "source": [ "six = Card(1, 6)\n", "print(six)" ] }, { "cell_type": "markdown", "id": "c5f66404", "metadata": {}, "source": [ "We can confirm that `queen` and `six` are not equivalent." ] }, { "cell_type": "code", "execution_count": 19, "id": "400c3340", "metadata": {}, "outputs": [], "source": [ "queen == six" ] }, { "cell_type": "markdown", "id": "1dcb561f", "metadata": {}, "source": [ "If we use the `!=` operator, Python invokes a special method called `__ne__`, if it exists.\n", "Otherwise it invokes`__eq__` and inverts the result -- so if `__eq__` returns `True`, the result of the `!=` operator is `False`." ] }, { "cell_type": "code", "execution_count": 20, "id": "c7d731b6", "metadata": {}, "outputs": [], "source": [ "queen != queen2" ] }, { "cell_type": "code", "execution_count": 21, "id": "d2be6c82", "metadata": {}, "outputs": [], "source": [ "queen != six" ] }, { "cell_type": "markdown", "id": "77c48464", "metadata": {}, "source": [ "Now suppose we want to compare two cards to see which is bigger.\n", "If we use one of the relational operators, we get a `TypeError`." ] }, { "cell_type": "code", "execution_count": 22, "id": "aa63fe2a", "metadata": { "tags": [] }, "outputs": [], "source": [ "\n", "queen < queen2" ] }, { "cell_type": "markdown", "id": "4db0ad52", "metadata": {}, "source": [ "To change the behavior of the `<` operator, we can define a special method called `__lt__`, which is short for \"less than\".\n", "For the sake of this example, let's assume that suit is more important than rank -- so all Spades outrank all Hearts, which outrank all Diamonds, and so on.\n", "If two cards have the same suit, the one with the higher rank wins.\n", "\n", "To implement this logic, we'll use the following method, which returns a tuple containing a card's suit and rank, in that order." ] }, { "cell_type": "code", "execution_count": 23, "id": "b2126f79", "metadata": {}, "outputs": [], "source": [ "%%add_method_to Card\n", "\n", " def to_tuple(self):\n", " return (self.suit, self.rank)" ] }, { "cell_type": "markdown", "id": "d5062348", "metadata": {}, "source": [ "We can use this method to write `__lt__`." ] }, { "cell_type": "code", "execution_count": 24, "id": "d4d0a652", "metadata": {}, "outputs": [], "source": [ "%%add_method_to Card\n", "\n", " def __lt__(self, other):\n", " return self.to_tuple() < other.to_tuple()" ] }, { "cell_type": "markdown", "id": "bd9ef8f5", "metadata": {}, "source": [ "Tuple comparison compares the first elements from each tuple, which represent the suits.\n", "If they are the same, it compares the second elements, which represent the ranks.\n", "\n", "Now if we use the `<` operator, it invokes the `__lt__` method." ] }, { "cell_type": "code", "execution_count": 25, "id": "9d4ea1f8", "metadata": {}, "outputs": [], "source": [ "six < queen" ] }, { "cell_type": "markdown", "id": "83289a77", "metadata": {}, "source": [ "If we use the `>` operator, it invokes a special method called `__gt__`, if it exists.\n", "Otherwise it invokes `__lt__` with the arguments in the opposite order." ] }, { "cell_type": "code", "execution_count": 26, "id": "676ede7e", "metadata": {}, "outputs": [], "source": [ "queen < queen2" ] }, { "cell_type": "code", "execution_count": 27, "id": "3c4854fb", "metadata": {}, "outputs": [], "source": [ "queen > queen2" ] }, { "cell_type": "markdown", "id": "5d0a91de", "metadata": {}, "source": [ "Finally, if we use the `<=` operator, it invokes a special method called `__le__`." ] }, { "cell_type": "code", "execution_count": 28, "id": "27280fc2", "metadata": {}, "outputs": [], "source": [ "%%add_method_to Card\n", "\n", " def __le__(self, other):\n", " return self.to_tuple() <= other.to_tuple()" ] }, { "cell_type": "markdown", "id": "6c85ac69", "metadata": {}, "source": [ "So we can check whether one card is less than or equal to another." ] }, { "cell_type": "code", "execution_count": 29, "id": "bea50d85", "metadata": {}, "outputs": [], "source": [ "queen <= queen2" ] }, { "cell_type": "code", "execution_count": 30, "id": "8d539454", "metadata": {}, "outputs": [], "source": [ "queen <= six" ] }, { "cell_type": "markdown", "id": "7af7b289", "metadata": {}, "source": [ "If we use the `>=` operator, it uses `__ge__` if it exists. Otherwise, it invokes `__le__` with the arguments in the opposite order." ] }, { "cell_type": "code", "execution_count": 31, "id": "e7edb7cb", "metadata": {}, "outputs": [], "source": [ "queen >= six" ] }, { "cell_type": "markdown", "id": "fe2a81cc", "metadata": {}, "source": [ "As we have defined them, these methods are complete in the sense that we can compare any two `Card` objects, and consistent in the sense that results from different operators don't contradict each other.\n", "With these two properties, we can say that `Card` objects are **totally ordered**.\n", "And that means, as we'll see soon, that they can be sorted." ] }, { "cell_type": "markdown", "id": "199f8bfc", "metadata": {}, "source": [ "## Decks\n", "\n", "Now that we have objects that represent cards, let's define objects that represent decks.\n", "The following is a class definition for `Deck` with\n", "an `__init__` method takes a list of `Card` objects as a parameter and assigns it to an attribute called `cards`." ] }, { "cell_type": "code", "execution_count": 32, "id": "b55140e3", "metadata": {}, "outputs": [], "source": [ "class Deck:\n", "\n", " def __init__(self, cards):\n", " self.cards = cards" ] }, { "cell_type": "markdown", "id": "2d529789", "metadata": {}, "source": [ "To create a list that contains the 52 cards in a standard deck, we'll use the following static method." ] }, { "cell_type": "code", "execution_count": 33, "id": "836f1a32", "metadata": {}, "outputs": [], "source": [ "%%add_method_to Deck\n", "\n", " def make_cards():\n", " cards = []\n", " for suit in range(4):\n", " for rank in range(2, 15):\n", " card = Card(suit, rank)\n", " cards.append(card)\n", " return cards" ] }, { "cell_type": "markdown", "id": "47ae8f71", "metadata": {}, "source": [ "In `make_cards`, the outer loop enumerates the suits from `0` to `3`.\n", "The inner loop enumerates the ranks from `2` to `14` -- where `14` represents an Ace that outranks a King.\n", "Each iteration creates a new `Card` with the current suit and rank, and appends it to `cards`.\n", "\n", "Here's how we make a list of cards and a `Deck` object that contains it." ] }, { "cell_type": "code", "execution_count": 34, "id": "ca50c79b", "metadata": {}, "outputs": [], "source": [ "cards = Deck.make_cards()\n", "deck = Deck(cards)\n", "len(deck.cards)" ] }, { "cell_type": "markdown", "id": "032ec302", "metadata": {}, "source": [ "It contains 52 cards, as intended." ] }, { "cell_type": "markdown", "id": "c2ec7f01", "metadata": { "tags": [] }, "source": [ "## Printing the deck\n", "\n", "Here is a `__str__` method for `Deck`." ] }, { "cell_type": "code", "execution_count": 35, "id": "1f1b923e", "metadata": {}, "outputs": [], "source": [ "%%add_method_to Deck\n", "\n", " def __str__(self):\n", " res = []\n", " for card in self.cards:\n", " res.append(str(card))\n", " return '\\n'.join(res)" ] }, { "cell_type": "markdown", "id": "660f18e6", "metadata": {}, "source": [ "This method demonstrates an efficient way to accumulate a large string -- building a list of strings and then using the string method `join`. \n", "\n", "We'll test this method with a deck that only contains two cards." ] }, { "cell_type": "code", "execution_count": 36, "id": "0c55a663", "metadata": {}, "outputs": [], "source": [ "small_deck = Deck([queen, six])" ] }, { "cell_type": "markdown", "id": "91c7145f", "metadata": {}, "source": [ "If we call `str`, it invokes `__str__`." ] }, { "cell_type": "code", "execution_count": 37, "id": "fb3350ef", "metadata": {}, "outputs": [], "source": [ "str(small_deck)" ] }, { "cell_type": "markdown", "id": "00270656", "metadata": {}, "source": [ "When Jupyter displays a string, it shows the \"representational\" form of the string, which represents a newline with the sequence `\\n`.\n", "\n", "However, if we print the result, Jupyter shows the \"printable\" form of the string, which prints the newline as whitespace." ] }, { "cell_type": "code", "execution_count": 38, "id": "d67f8fd5", "metadata": {}, "outputs": [], "source": [ "print(small_deck)" ] }, { "cell_type": "markdown", "id": "e97810c4", "metadata": {}, "source": [ "So the cards appear on separate lines." ] }, { "cell_type": "markdown", "id": "52d3d597", "metadata": {}, "source": [ "## Add, remove, shuffle and sort\n", "\n", "To deal cards, we would like a method that removes a card from the deck\n", "and returns it. The list method `pop` provides a convenient way to do\n", "that." ] }, { "cell_type": "code", "execution_count": 39, "id": "3836c48c", "metadata": {}, "outputs": [], "source": [ "%%add_method_to Deck\n", "\n", " def take_card(self):\n", " return self.cards.pop()" ] }, { "cell_type": "markdown", "id": "1fcef47b", "metadata": {}, "source": [ "Here's how we use it." ] }, { "cell_type": "code", "execution_count": 40, "id": "5afccad6", "metadata": {}, "outputs": [], "source": [ "card = deck.take_card()\n", "print(card)" ] }, { "cell_type": "markdown", "id": "65427954", "metadata": {}, "source": [ "We can confirm that there are `51` cards left in the deck." ] }, { "cell_type": "code", "execution_count": 41, "id": "58f9473a", "metadata": {}, "outputs": [], "source": [ "len(deck.cards)" ] }, { "cell_type": "markdown", "id": "7ca3614e", "metadata": {}, "source": [ "To add a card, we can use the list method `append`." ] }, { "cell_type": "code", "execution_count": 42, "id": "f3eac4b5", "metadata": {}, "outputs": [], "source": [ "%%add_method_to Deck\n", "\n", " def put_card(self, card):\n", " self.cards.append(card)" ] }, { "cell_type": "markdown", "id": "2ecd8703", "metadata": {}, "source": [ "As an example, we can put back the card we just popped." ] }, { "cell_type": "code", "execution_count": 43, "id": "f234eff4", "metadata": {}, "outputs": [], "source": [ "deck.put_card(card)\n", "len(deck.cards)" ] }, { "cell_type": "markdown", "id": "8b5af8ce", "metadata": {}, "source": [ "To shuffle the deck, we can use the `shuffle` function from the `random` module:" ] }, { "cell_type": "code", "execution_count": 44, "id": "81e60a08", "metadata": {}, "outputs": [], "source": [ "import random" ] }, { "cell_type": "code", "execution_count": 45, "id": "b630cbb8", "metadata": { "tags": [] }, "outputs": [], "source": [ "# This cell initializes the random number generator so we\n", "# always get the same results.\n", "\n", "random.seed(3)" ] }, { "cell_type": "code", "execution_count": 46, "id": "bea615ea", "metadata": {}, "outputs": [], "source": [ "%%add_method_to Deck\n", " \n", " def shuffle(self):\n", " random.shuffle(self.cards)" ] }, { "cell_type": "markdown", "id": "a8cb1a7f", "metadata": {}, "source": [ "If we shuffle the deck and print the first few cards, we can see that they are in no apparent order." ] }, { "cell_type": "code", "execution_count": 47, "id": "6b0f6b02", "metadata": {}, "outputs": [], "source": [ "deck.shuffle()\n", "for card in deck.cards[:4]:\n", " print(card)" ] }, { "cell_type": "markdown", "id": "a198dde3", "metadata": {}, "source": [ "To sort the cards, we can use the list method `sort`, which sorts the elements \"in place\" -- that is, it modifies the list rather than creating a new list." ] }, { "cell_type": "code", "execution_count": 48, "id": "6bff10b6", "metadata": {}, "outputs": [], "source": [ "%%add_method_to Deck\n", " \n", " def sort(self):\n", " self.cards.sort()" ] }, { "cell_type": "markdown", "id": "d4f017c7", "metadata": {}, "source": [ "When we invoke `sort`, it uses the `__lt__` method to compare cards." ] }, { "cell_type": "code", "execution_count": 49, "id": "568a6583", "metadata": {}, "outputs": [], "source": [ "deck.sort()" ] }, { "cell_type": "markdown", "id": "2bb966fd", "metadata": {}, "source": [ "If we print the first few cards, we can confirm that they are in increasing order." ] }, { "cell_type": "code", "execution_count": 50, "id": "54e91c91", "metadata": {}, "outputs": [], "source": [ "for card in deck.cards[:4]:\n", " print(card)" ] }, { "cell_type": "markdown", "id": "5c41ce4d", "metadata": {}, "source": [ "In this example, `Deck.sort` doesn't do anything other than invoke `list.sort`.\n", "Passing along responsibility like this is called **delegation**." ] }, { "cell_type": "markdown", "id": "0502961b", "metadata": {}, "source": [ "## Parents and children\n", "\n", "Inheritance is the ability to define a new class that is a modified version of an existing class.\n", "As an example, let's say we want a class to represent a \"hand\", that is, the cards held by one player.\n", "\n", "* A hand is similar to a deck -- both are made up of a collection of cards, and both require operations like adding and removing cards.\n", "\n", "* A hand is also different from a deck -- there are operations we want for hands that don't make sense for a deck. For example, in poker we might compare two hands to see which one wins. In bridge, we might compute a score for a hand in order to make a bid.\n", "\n", "This relationship between classes -- where one is a specialized version of another -- lends itself to inheritance. \n", "\n", "To define a new class that is based on an existing class, we put the name of the existing class in parentheses." ] }, { "cell_type": "code", "execution_count": 51, "id": "f39fc598", "metadata": {}, "outputs": [], "source": [ "class Hand(Deck):\n", " \"\"\"Represents a hand of playing cards.\"\"\"" ] }, { "cell_type": "markdown", "id": "339295cd", "metadata": {}, "source": [ "This definition indicates that `Hand` inherits from `Deck`, which means that `Hand` objects can access methods defined in `Deck`, like `take_card` and `put_card`.\n", "\n", "`Hand` also inherits `__init__` from `Deck`, but if we define `__init__` in the `Hand` class, it overrides the one in the `Deck` class." ] }, { "cell_type": "code", "execution_count": 52, "id": "9e7a1045", "metadata": {}, "outputs": [], "source": [ "%%add_method_to Hand\n", "\n", " def __init__(self, label=''):\n", " self.label = label\n", " self.cards = []" ] }, { "cell_type": "markdown", "id": "9b6a763a", "metadata": {}, "source": [ "This version of `__init__` takes an optional string as a parameter, and always starts with an empty list of cards.\n", "When we create a `Hand`, Python invokes this method, not the one in `Deck` -- which we can confirm by checking that the result has a `label` attribute." ] }, { "cell_type": "code", "execution_count": 53, "id": "8de8cff4", "metadata": {}, "outputs": [], "source": [ "hand = Hand('player 1')\n", "hand.label" ] }, { "cell_type": "markdown", "id": "b1e2a67d", "metadata": {}, "source": [ "To deal a card, we can use `take_card` to remove a card from a `Deck`, and `put_card` to add the card to a `Hand`." ] }, { "cell_type": "code", "execution_count": 54, "id": "9f582ce0", "metadata": {}, "outputs": [], "source": [ "deck = Deck(cards)\n", "card = deck.take_card()\n", "hand.put_card(card)\n", "print(hand)" ] }, { "cell_type": "markdown", "id": "dc2ce06b", "metadata": {}, "source": [ "Let's encapsulate this code in a `Deck` method called `move_cards`." ] }, { "cell_type": "code", "execution_count": 55, "id": "180069eb", "metadata": {}, "outputs": [], "source": [ "%%add_method_to Deck\n", "\n", " def move_cards(self, other, num):\n", " for i in range(num):\n", " card = self.take_card()\n", " other.put_card(card)" ] }, { "cell_type": "markdown", "id": "16e6c404", "metadata": {}, "source": [ "This method is polymorphic -- that is, it works with more than one type: `self` and `other` can be either a `Hand` or a `Deck`.\n", "So we can use this method to deal a card from `Deck` to a `Hand`, from one `Hand` to another, or from a `Hand` back to a `Deck`." ] }, { "cell_type": "markdown", "id": "e648a722", "metadata": {}, "source": [ "When a new class inherits from an existing one, the existing one is called the **parent** and the new class is called the **child**. In general:\n", "\n", "* Instances of the child class should have all of the attributes of the parent class, but they can have additional attributes.\n", "\n", "* The child class should have all of the methods of the parent class, but it can have additional methods.\n", "\n", "* If a child class overrides a method from the parent class, the new method should take the same parameters and return a compatible result.\n", "\n", "This set of rules is called the \"Liskov substitution principle\" after computer scientist Barbara Liskov.\n", "\n", "If you follow these rules, any function or method designed to work with an instance of a parent class, like a `Deck`, will also work with instances of a child class, like `Hand`.\n", "If you violate these rules, your code will collapse like a house of cards (sorry)." ] }, { "cell_type": "markdown", "id": "e80873dd", "metadata": {}, "source": [ "## Specialization\n", "\n", "Let's make a class called `BridgeHand` that represents a hand in bridge -- a widely played card game.\n", "We'll inherit from `Hand` and add a new method called `high_card_point_count` that evaluates a hand using a \"high card point\" method, which adds up points for the high cards in the hand.\n", "\n", "Here's a class definition that contains as a class variable a dictionary that maps from card names to their point values." ] }, { "cell_type": "code", "execution_count": 56, "id": "b9e949cf", "metadata": {}, "outputs": [], "source": [ "class BridgeHand(Hand):\n", " \"\"\"Represents a bridge hand.\"\"\"\n", "\n", " hcp_dict = {\n", " 'Ace': 4,\n", " 'King': 3,\n", " 'Queen': 2,\n", " 'Jack': 1,\n", " }" ] }, { "cell_type": "markdown", "id": "4c038717", "metadata": {}, "source": [ "Given the rank of a card, like `12`, we can use `Card.rank_names` to get the string representation of the rank, and then use `hcp_dict` to get its score." ] }, { "cell_type": "code", "execution_count": 57, "id": "0586b764", "metadata": {}, "outputs": [], "source": [ "rank = 12\n", "rank_name = Card.rank_names[rank]\n", "score = BridgeHand.hcp_dict.get(rank_name, 0)\n", "rank_name, score" ] }, { "cell_type": "markdown", "id": "c3a7820d", "metadata": {}, "source": [ "The following method loops through the cards in a `BridgeHand` and adds up their scores." ] }, { "cell_type": "code", "execution_count": 58, "id": "f01e8027", "metadata": {}, "outputs": [], "source": [ "%%add_method_to BridgeHand\n", "\n", " def high_card_point_count(self):\n", " count = 0\n", " for card in self.cards:\n", " rank_name = Card.rank_names[card.rank]\n", " count += BridgeHand.hcp_dict.get(rank_name, 0)\n", " return count" ] }, { "cell_type": "code", "execution_count": 59, "id": "227bef61", "metadata": { "tags": [] }, "outputs": [], "source": [ "# This cell makes a fresh Deck and \n", "# initializes the random number generator\n", "\n", "cards = Deck.make_cards()\n", "deck = Deck(cards)\n", "random.seed(3)" ] }, { "cell_type": "markdown", "id": "94535d8e", "metadata": {}, "source": [ "To test it, we'll deal a hand with five cards -- a bridge hand usually has thirteen, but it's easier to test code with small examples." ] }, { "cell_type": "code", "execution_count": 60, "id": "f7d9275a", "metadata": {}, "outputs": [], "source": [ "hand = BridgeHand('player 2')\n", "\n", "deck.shuffle()\n", "deck.move_cards(hand, 5)\n", "print(hand)" ] }, { "cell_type": "markdown", "id": "a1bd2521", "metadata": {}, "source": [ "And here is the total score for the King and Queen." ] }, { "cell_type": "code", "execution_count": 61, "id": "ceb63a74", "metadata": {}, "outputs": [], "source": [ "hand.high_card_point_count()" ] }, { "cell_type": "markdown", "id": "b4f5e107", "metadata": {}, "source": [ "`BridgeHand` inherits the variables and methods of `Hand` and adds a class variable and a method that are specific to bridge.\n", "This way of using inheritance is called **specialization** because it defines a new class that is specialized for a particular use, like playing bridge." ] }, { "cell_type": "markdown", "id": "b493622d", "metadata": {}, "source": [ "## Debugging\n", "\n", "Inheritance is a useful feature.\n", "Some programs that would be repetitive without inheritance can be written more concisely with it.\n", "Also, inheritance can facilitate code reuse, since you can customize the behavior of a parent class without having to modify it.\n", "In some cases, the inheritance structure reflects the natural structure of the problem, which makes the design easier to understand.\n", "\n", "On the other hand, inheritance can make programs difficult to read.\n", "When a method is invoked, it is sometimes not clear where to find its definition -- the relevant code may be spread across several modules.\n", "\n", "Any time you are unsure about the flow of execution through your program, the simplest solution is to add print statements at the beginning of the relevant methods.\n", "If `Deck.shuffle` prints a message that says something like `Running Deck.shuffle`, then as the program runs it traces the flow of execution.\n", "\n", "As an alternative, you could use the following function, which takes an object and a method name (as a string) and returns the class that provides the definition of the method." ] }, { "cell_type": "code", "execution_count": 62, "id": "20633d57", "metadata": {}, "outputs": [], "source": [ "def find_defining_class(obj, method_name):\n", " \"\"\"Find the class where the given method is defined.\"\"\"\n", " for typ in type(obj).mro():\n", " if method_name in vars(typ):\n", " return typ\n", " return f'Method {method_name} not found.'" ] }, { "cell_type": "markdown", "id": "1ee8f2da", "metadata": {}, "source": [ "`find_defining_class` uses the `mro` method to get the list of class objects (types) that will be searched for methods.\n", "\"MRO\" stands for \"method resolution order\", which is the sequence of classes Python searches to \"resolve\" a method name -- that is, to find the function object the name refers to.\n", "\n", "As an example, let's instantiate a `BridgeHand` and then find the defining class of `shuffle`." ] }, { "cell_type": "code", "execution_count": 63, "id": "c5bec04f", "metadata": {}, "outputs": [], "source": [ "hand = BridgeHand('player 3')\n", "find_defining_class(hand, 'shuffle')" ] }, { "cell_type": "markdown", "id": "eeb70a14", "metadata": {}, "source": [ "The `shuffle` method for the `BridgeHand` object is the one in `Deck`." ] }, { "cell_type": "markdown", "id": "07f4c4bb", "metadata": {}, "source": [ "## Glossary\n", "\n", "**inheritance:**\n", " The ability to define a new class that is a modified version of a previously defined class.\n", "\n", "**encode:**\n", " To represent one set of values using another set of values by constructing a mapping between them.\n", "\n", "**class variable:**\n", "A variable defined inside a class definition, but not inside any method.\n", "\n", "**totally ordered:**\n", "A set of objects is totally ordered if we can compare any two elements and the results are consistent.\n", "\n", "**delegation:**\n", "When one method passes responsibility to another method to do most or all of the work.\n", "\n", "**parent class:**\n", "A class that is inherited from.\n", "\n", "**child class:**\n", "A class that inherits from another class.\n", "\n", "**specialization:**\n", "A way of using inheritance to create a new class that is a specialized version of an existing class." ] }, { "cell_type": "markdown", "id": "0ab6f8d3", "metadata": {}, "source": [ "\n", "**See also:** the [full vocabulary glossary](https://python.porttack.com/alignment/ap-vocabulary-glossary.html) collects every term in this book, alphabetized, alongside the complete AP CSP exam vocabulary list.\n", "" ] }, { "cell_type": "markdown", "id": "1aea9b2b", "metadata": {}, "source": [ "## Exercises" ] }, { "cell_type": "code", "execution_count": 64, "id": "e281457a", "metadata": { "tags": [] }, "outputs": [], "source": [ "# This cell tells Jupyter to provide detailed debugging information\n", "# when a runtime error occurs. Run it before working on the exercises.\n", "\n", "%xmode Verbose" ] }, { "cell_type": "markdown", "id": "7913e6b1", "metadata": {}, "source": [ "### Ask a Virtual Assistant\n", "\n", "When it goes well, object-oriented programming can make programs more readable, testable, and reusable.\n", "But it can also make programs complicated and hard to maintain.\n", "As a result, OOP is a topic of controversy -- some people love it, and some people don't.\n", "\n", "To learn more about the topic, ask a virtual assistant:\n", "\n", "* What are some pros and cons of object-oriented programming?\n", "\n", "* What does it mean when people say \"favor composition over inheritance\"?\n", "\n", "* What is the Liskov substitution principle?\n", "\n", "* Is Python an object-oriented language?\n", "\n", "* What are the requirements for a set to be totally ordered?\n", "\n", "And as always, consider using a virtual assistant to help with the following exercises." ] }, { "cell_type": "markdown", "id": "1af81269", "metadata": {}, "source": [ "### Exercise\n", "\n", "In contract bridge, a \"trick\" is a round of play in which each of four players plays one card.\n", "To represent those cards, we'll define a class that inherits from `Deck`." ] }, { "cell_type": "code", "execution_count": 65, "id": "47d9ce31", "metadata": {}, "outputs": [], "source": [ "class Trick(Deck):\n", " \"\"\"Represents a trick in contract bridge.\"\"\"" ] }, { "cell_type": "markdown", "id": "9916d562", "metadata": {}, "source": [ "As an example, consider this trick, where the first player leads with the 3 of Diamonds, which means that Diamonds are the \"led suit\".\n", "The second and third players \"follow suit\", which means they play a card with the led suit.\n", "The fourth player plays a card of a different suit, which means they cannot win the trick.\n", "So the winner of this trick is the third player, because they played the highest card in the led suit." ] }, { "cell_type": "code", "execution_count": 66, "id": "7bb54059", "metadata": {}, "outputs": [], "source": [ "cards = [Card(1, 3),\n", " Card(1, 10),\n", " Card(1, 12),\n", " Card(2, 13)]\n", "trick = Trick(cards)\n", "print(trick)" ] }, { "cell_type": "markdown", "id": "c94a1337", "metadata": {}, "source": [ "Write a `Trick` method called `find_winner` that loops through the cards in the `Trick` and returns the index of the card that wins.\n", "In the previous example, the index of the winning card is `2`." ] }, { "cell_type": "markdown", "id": "60c43389", "metadata": { "tags": [] }, "source": [ "You can use the following outline to get started." ] }, { "cell_type": "code", "execution_count": 67, "id": "72a410d7", "metadata": { "tags": [] }, "outputs": [], "source": [ "%%add_method_to Trick\n", "\n", " def find_winner(self):\n", " return 0" ] }, { "cell_type": "code", "execution_count": 68, "id": "b09cceb1", "metadata": { "tags": [] }, "outputs": [], "source": [] }, { "cell_type": "markdown", "id": "d7e3b94c", "metadata": { "tags": [] }, "source": [ "If you test your method with the previous example, the index of the winning card should be `2`." ] }, { "cell_type": "code", "execution_count": 69, "id": "e185c2f7", "metadata": { "tags": [] }, "outputs": [], "source": [ "trick.find_winner()" ] }, { "cell_type": "markdown", "id": "b5b9fb4b", "metadata": {}, "source": [ "### Exercise\n", "\n", "The next few exercises ask to you write functions that classify poker hands.\n", "If you are not familiar with poker, I'll explain what you need to know.\n", "We'll use the following class to represent poker hands." ] }, { "cell_type": "code", "execution_count": 70, "id": "2558ddd1", "metadata": {}, "outputs": [], "source": [ "class PokerHand(Hand):\n", " \"\"\"Represents a poker hand.\"\"\"\n", "\n", " def get_suit_counts(self):\n", " counter = {}\n", " for card in self.cards:\n", " key = card.suit\n", " counter[key] = counter.get(key, 0) + 1\n", " return counter\n", " \n", " def get_rank_counts(self):\n", " counter = {}\n", " for card in self.cards:\n", " key = card.rank\n", " counter[key] = counter.get(key, 0) + 1\n", " return counter " ] }, { "cell_type": "markdown", "id": "2daecced", "metadata": {}, "source": [ "`PokerHand` provides two methods that will help with the exercises.\n", "\n", "* `get_suit_counts` loops through the cards in the `PokerHand`, counts the number of cards in each suit, and returns a dictionary that maps from each suit code to the number of times it appears.\n", "\n", "* `get_rank_counts` does the same thing with the ranks of the cards, returning a dictionary that maps from each rank code to the number of times it appears.\n", "\n", "All of the exercises that follow can be done using only the Python features we have learned so far, but some of them are more difficult than most of the previous exercises.\n", "I encourage you to ask a virtual assistant for help.\n", "\n", "For problems like this, it often works well to ask for general advice about strategies and algorithms.\n", "Then you can either write the code yourself or ask for code.\n", "If you ask for code, you might want to provide the relevant class definitions as part of the prompt." ] }, { "cell_type": "markdown", "id": "ccc2d8ca", "metadata": {}, "source": [ "As a first exercise, we'll write a method called `has_flush` that checks whether a hand has a \"flush\" -- that is, whether it contains at least five cards of the same suit.\n", "\n", "In most varieties of poker, a hand contains either five or seven cards, but there are some exotic variations where a hand contains other numbers of cards.\n", "But regardless of how many cards there are in a hand, the only ones that count are the five that make the best hand." ] }, { "cell_type": "markdown", "id": "6911a0f6", "metadata": { "tags": [] }, "source": [ "You can use the following outline to get started." ] }, { "cell_type": "code", "execution_count": 71, "id": "7aa25f29", "metadata": { "tags": [] }, "outputs": [], "source": [ "%%add_method_to PokerHand\n", "\n", " def has_flush(self):\n", " \"\"\"Checks whether this hand has a flush.\"\"\"\n", " return False" ] }, { "cell_type": "code", "execution_count": 72, "id": "72c769d5", "metadata": { "tags": [] }, "outputs": [], "source": [] }, { "cell_type": "markdown", "id": "07878cb3", "metadata": { "tags": [] }, "source": [ "To test this method, we'll construct a hand with five cards that are all Clubs, so it contains a flush." ] }, { "cell_type": "code", "execution_count": 73, "id": "e1c4f474", "metadata": { "tags": [] }, "outputs": [], "source": [ "good_hand = PokerHand('good_hand')\n", "\n", "suit = 0\n", "for rank in range(10, 15):\n", " card = Card(suit, rank)\n", " good_hand.put_card(card)\n", " \n", "print(good_hand)" ] }, { "cell_type": "markdown", "id": "83698207", "metadata": { "tags": [] }, "source": [ "If we invoke `get_suit_counts`, we can confirm that the rank code `0` appears `5` times." ] }, { "cell_type": "code", "execution_count": 74, "id": "c91e3bdb", "metadata": { "tags": [] }, "outputs": [], "source": [ "good_hand.get_suit_counts()" ] }, { "cell_type": "markdown", "id": "fdec7276", "metadata": { "tags": [] }, "source": [ "So `has_flush` should return `True`." ] }, { "cell_type": "code", "execution_count": 75, "id": "a60e233d", "metadata": { "tags": [] }, "outputs": [], "source": [ "good_hand.has_flush()" ] }, { "cell_type": "markdown", "id": "ba10e1ba", "metadata": { "tags": [] }, "source": [ "As a second test, we'll construct a hand with three Clubs and two other suits." ] }, { "cell_type": "code", "execution_count": 76, "id": "ae5063e2", "metadata": { "tags": [] }, "outputs": [], "source": [ "cards = [Card(0, 2),\n", " Card(0, 3),\n", " Card(2, 4),\n", " Card(3, 5),\n", " Card(0, 7),\n", " ]\n", "\n", "bad_hand = PokerHand('bad hand')\n", "for card in cards:\n", " bad_hand.put_card(card)\n", " \n", "print(bad_hand)" ] }, { "cell_type": "markdown", "id": "8611610f", "metadata": { "tags": [] }, "source": [ "So `has_flush` should return `False`." ] }, { "cell_type": "code", "execution_count": 77, "id": "940928ba", "metadata": { "tags": [] }, "outputs": [], "source": [ "bad_hand.has_flush()" ] }, { "cell_type": "markdown", "id": "ad716880", "metadata": {}, "source": [ "### Exercise\n", "\n", "Write a method called `has_straight` that checks whether a hand contains a straight, which is a set of five cards with consecutive ranks.\n", "For example, if a hand contains ranks `5`, `6`, `7`, `8`, and `9`, it contains a straight.\n", "\n", "An Ace can come before a two or after a King, so `Ace`, `2`, `3`, `4`, `5` is a straight and so is `10`, `Jack`, `Queen`, `King`, `Ace`.\n", "But a straight cannot \"wrap around\", so `King`, `Ace`, `2`, `3`, `4` is not a straight." ] }, { "cell_type": "markdown", "id": "bbd04e50", "metadata": { "tags": [] }, "source": [ "You can use the following outline to get started.\n", "It includes a few lines of code that count the number of Aces -- represented with the code `1` or `14` -- and store the total in both locations of the counter." ] }, { "cell_type": "code", "execution_count": 78, "id": "6513ef14", "metadata": { "tags": [] }, "outputs": [], "source": [ "%%add_method_to PokerHand\n", "\n", " def has_straight(self, n=5):\n", " \"\"\"Checks whether this hand has a straight with at least `n` cards.\"\"\"\n", " counter = self.get_rank_counts()\n", " aces = counter.get(1, 0) + counter.get(14, 0)\n", " counter[1] = aces\n", " counter[14] = aces\n", " \n", " return False" ] }, { "cell_type": "code", "execution_count": 79, "id": "8a220d67", "metadata": { "tags": [] }, "outputs": [], "source": [] }, { "cell_type": "markdown", "id": "d2e576bf", "metadata": { "tags": [] }, "source": [ "`good_hand`, which we created for the previous exercise, contains a straight.\n", "If we use `get_rank_counts`, we can confirm that it has at least one card with each of five consecutive ranks." ] }, { "cell_type": "code", "execution_count": 80, "id": "5a422cae", "metadata": { "tags": [] }, "outputs": [], "source": [ "good_hand.get_rank_counts()" ] }, { "cell_type": "markdown", "id": "009a20dd", "metadata": { "tags": [] }, "source": [ "So `has_straight` should return `True`." ] }, { "cell_type": "code", "execution_count": 81, "id": "24483b99", "metadata": { "tags": [] }, "outputs": [], "source": [ "good_hand.has_straight()" ] }, { "cell_type": "markdown", "id": "1d79b2b6", "metadata": { "tags": [] }, "source": [ "`bad_hand` does not contain a straight, so `has_straight` should return `False`." ] }, { "cell_type": "code", "execution_count": 82, "id": "69f7cea9", "metadata": { "tags": [] }, "outputs": [], "source": [ "bad_hand.has_straight()" ] }, { "cell_type": "markdown", "id": "c1ecebd3", "metadata": {}, "source": [ "### Exercise\n", "\n", "A hand has a straight flush if it contains a set of five cards that are both a straight and a flush -- that is, five cards of the same suit with consecutive ranks.\n", "Write a `PokerHand` method that checks whether a hand has a straight flush." ] }, { "cell_type": "markdown", "id": "94570008", "metadata": { "tags": [] }, "source": [ "You can use the following outline to get started." ] }, { "cell_type": "code", "execution_count": 83, "id": "88bd35fb", "metadata": { "tags": [] }, "outputs": [], "source": [ "%%add_method_to PokerHand\n", "\n", " def has_straightflush(self):\n", " \"\"\"Check whether this hand has a straight flush.\"\"\"\n", " return False" ] }, { "cell_type": "code", "execution_count": 84, "id": "5e602773", "metadata": { "tags": [] }, "outputs": [], "source": [] }, { "cell_type": "code", "execution_count": 85, "id": "997b120d", "metadata": { "tags": [] }, "outputs": [], "source": [] }, { "cell_type": "markdown", "id": "4a1e17b1", "metadata": { "tags": [] }, "source": [ "Use the following examples to test your method." ] }, { "cell_type": "code", "execution_count": 86, "id": "23704ab4", "metadata": { "tags": [] }, "outputs": [], "source": [ "good_hand.has_straightflush() # should return True" ] }, { "cell_type": "code", "execution_count": 87, "id": "7d8d2fdb", "metadata": { "tags": [] }, "outputs": [], "source": [ "bad_hand.has_straightflush() # should return False" ] }, { "cell_type": "markdown", "id": "9127ceb1", "metadata": { "tags": [] }, "source": [ "Note that it is not enough to check whether a hand has a straight and a flush.\n", "To see why, consider the following hand." ] }, { "cell_type": "code", "execution_count": 88, "id": "4c5727d9", "metadata": { "tags": [] }, "outputs": [], "source": [ "from copy import deepcopy\n", "\n", "straight_and_flush = deepcopy(bad_hand)\n", "straight_and_flush.put_card(Card(0, 6))\n", "straight_and_flush.put_card(Card(0, 9))\n", "print(straight_and_flush)" ] }, { "cell_type": "markdown", "id": "c4ed18e4", "metadata": { "tags": [] }, "source": [ "This hand contains a straight and a flush, but they are not the same five cards." ] }, { "cell_type": "code", "execution_count": 89, "id": "7e65e951", "metadata": { "tags": [] }, "outputs": [], "source": [ " straight_and_flush.has_straight(), straight_and_flush.has_flush()" ] }, { "cell_type": "markdown", "id": "5ff4e160", "metadata": { "tags": [] }, "source": [ "So it does not contain a straight flush." ] }, { "cell_type": "code", "execution_count": 90, "id": "758fc922", "metadata": { "tags": [] }, "outputs": [], "source": [ "straight_and_flush.has_straightflush() # should return False" ] }, { "cell_type": "markdown", "id": "dd742401", "metadata": {}, "source": [ "### Exercise\n", "\n", "A poker hand has a pair if it contains two or more cards with the same rank.\n", "Write a `PokerHand` method that checks whether a hand contains a pair." ] }, { "cell_type": "markdown", "id": "00464353", "metadata": { "tags": [] }, "source": [ "You can use the following outline to get started." ] }, { "cell_type": "code", "execution_count": 91, "id": "15a81a40", "metadata": { "tags": [] }, "outputs": [], "source": [ "%%add_method_to PokerHand\n", "\n", " def check_sets(self, *need_list):\n", " return True" ] }, { "cell_type": "code", "execution_count": 92, "id": "bc7ca211", "metadata": { "tags": [] }, "outputs": [], "source": [] }, { "cell_type": "code", "execution_count": 93, "id": "20b65d89", "metadata": { "tags": [] }, "outputs": [], "source": [] }, { "cell_type": "markdown", "id": "9f001207", "metadata": {}, "source": [ "To test your method, here's a hand that has a pair." ] }, { "cell_type": "code", "execution_count": 94, "id": "57e616be", "metadata": {}, "outputs": [], "source": [ "pair = deepcopy(bad_hand)\n", "pair.put_card(Card(1, 2))\n", "print(pair)" ] }, { "cell_type": "code", "execution_count": 95, "id": "b8d87fd8", "metadata": {}, "outputs": [], "source": [ "pair.has_pair() # should return True" ] }, { "cell_type": "code", "execution_count": 96, "id": "3016e99a", "metadata": {}, "outputs": [], "source": [ "bad_hand.has_pair() # should return False" ] }, { "cell_type": "code", "execution_count": 97, "id": "852e3b11", "metadata": {}, "outputs": [], "source": [ "good_hand.has_pair() # should return False" ] }, { "cell_type": "markdown", "id": "c4180a64", "metadata": {}, "source": [ "### Exercise\n", "\n", "A hand has a full house if it contains three cards of one rank and two cards of another rank.\n", "Write a `PokerHand` method that checks whether a hand has a full house." ] }, { "cell_type": "markdown", "id": "5aa80ee6", "metadata": { "tags": [] }, "source": [ "You can use the following outline to get started." ] }, { "cell_type": "code", "execution_count": 98, "id": "1f95601d", "metadata": { "tags": [] }, "outputs": [], "source": [ "%%add_method_to PokerHand\n", "\n", " def has_full_house(self):\n", " return False" ] }, { "cell_type": "code", "execution_count": 99, "id": "420ca0fd", "metadata": { "tags": [] }, "outputs": [], "source": [] }, { "cell_type": "markdown", "id": "f020cf9e", "metadata": { "tags": [] }, "source": [ "You can use this hand to test your method." ] }, { "cell_type": "code", "execution_count": 100, "id": "1e4957cb", "metadata": { "tags": [] }, "outputs": [], "source": [ "boat = deepcopy(pair)\n", "boat.put_card(Card(2, 2))\n", "boat.put_card(Card(2, 3))\n", "print(boat)" ] }, { "cell_type": "code", "execution_count": 101, "id": "4dc8b899", "metadata": { "tags": [] }, "outputs": [], "source": [ "boat.has_full_house() # should return True" ] }, { "cell_type": "code", "execution_count": 102, "id": "86229763", "metadata": { "tags": [] }, "outputs": [], "source": [ "pair.has_full_house() # should return False" ] }, { "cell_type": "code", "execution_count": 103, "id": "7f1b3664", "metadata": { "tags": [] }, "outputs": [], "source": [ "good_hand.has_full_house() # should return False" ] }, { "cell_type": "markdown", "id": "666340c1", "metadata": {}, "source": [ "### Exercise\n", "\n", "This exercise is a cautionary tale about a common error that can be difficult to debug.\n", "Consider the following class definition." ] }, { "cell_type": "code", "execution_count": 104, "id": "85b47651", "metadata": {}, "outputs": [], "source": [ "class Kangaroo:\n", " \"\"\"A Kangaroo is a marsupial.\"\"\"\n", " \n", " def __init__(self, name, contents=[]):\n", " \"\"\"Initialize the pouch contents.\n", "\n", " name: string\n", " contents: initial pouch contents.\n", " \"\"\"\n", " self.name = name\n", " self.contents = contents\n", "\n", " def __str__(self):\n", " \"\"\"Return a string representaion of this Kangaroo.\n", " \"\"\"\n", " t = [ self.name + ' has pouch contents:' ]\n", " for obj in self.contents:\n", " s = ' ' + object.__str__(obj)\n", " t.append(s)\n", " return '\\n'.join(t)\n", "\n", " def put_in_pouch(self, item):\n", " \"\"\"Adds a new item to the pouch contents.\n", "\n", " item: object to be added\n", " \"\"\"\n", " self.contents.append(item)" ] }, { "cell_type": "markdown", "id": "1e349832", "metadata": {}, "source": [ "`__init__` takes two parameters: `name` is required, but `contents` is optional -- if it's not provided, the default value is an empty list.\n", "\n", "`__str__` returns a string representation of the object that includes the name and the contents of the pouch.\n", "\n", "`put_in_pouch` takes any object and appends it to `contents`.\n", "\n", "Now let's see how this class works.\n", "We'll create two `Kangaroo` objects with the names `'Kanga'` and `'Roo'`." ] }, { "cell_type": "code", "execution_count": 105, "id": "13f1e1f3", "metadata": {}, "outputs": [], "source": [ "kanga = Kangaroo('Kanga')\n", "roo = Kangaroo('Roo')" ] }, { "cell_type": "markdown", "id": "533982d1", "metadata": {}, "source": [ "To Kanga's pouch we'll add two strings and Roo." ] }, { "cell_type": "code", "execution_count": 106, "id": "e882e2d6", "metadata": {}, "outputs": [], "source": [ "kanga.put_in_pouch('wallet')\n", "kanga.put_in_pouch('car keys')\n", "kanga.put_in_pouch(roo)" ] }, { "cell_type": "markdown", "id": "41cd6d6e", "metadata": {}, "source": [ "If we print `kanga`, it seems like everything worked." ] }, { "cell_type": "code", "execution_count": 107, "id": "724805bb", "metadata": {}, "outputs": [], "source": [ "print(kanga)" ] }, { "cell_type": "markdown", "id": "0ba26163", "metadata": {}, "source": [ "But what happens if we print `roo`?" ] }, { "cell_type": "code", "execution_count": 108, "id": "3f6cff16", "metadata": {}, "outputs": [], "source": [ "print(roo)" ] }, { "cell_type": "markdown", "id": "a2aef813", "metadata": {}, "source": [ "Roo's pouch contains the same contents as Kanga's, including a reference to `roo`!\n", "\n", "See if you can figure out what went wrong.\n", "Then ask a virtual assistant, \"What's wrong with the following program?\" and paste in the definition of `Kangaroo`." ] }, { "cell_type": "code", "execution_count": null, "id": "6ef15c8c", "metadata": {}, "outputs": [], "source": [] }, { "cell_type": "markdown", "id": "a7f4edf8", "metadata": { "tags": [] }, "source": [ "\n", "**Working in Python** — modified by Eric Brown for a high school Computer Science Principles class. Source and modifications: [github.com/porttack/working-in-python](https://github.com/porttack/working-in-python)\n", "\n", "---\n", "\n", "\n", "[Think Python: 3rd Edition](https://allendowney.github.io/ThinkPython/index.html)\n", "\n", "Copyright 2024 [Allen B. Downey](https://allendowney.com)\n", "\n", "Code license: [MIT License](https://mit-license.org/)\n", "\n", "Text license: [Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International](https://creativecommons.org/licenses/by-nc-sa/4.0/)" ] } ], "metadata": { "celltoolbar": "Tags", "kernelspec": { "display_name": "Python 3 (ipykernel)", "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.10.11" } }, "nbformat": 4, "nbformat_minor": 5 }