{ "cells": [ { "cell_type": "raw", "metadata": { "raw_mimetype": "text/restructuredtext" }, "source": [ ".. meta::\n", " :description: Topic: String Encoding Exercise, Difficulty: Medium, Category: Practice Problem\n", " :keywords: function, string, casting, practice problem" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Encode as String\n", "Sometimes it is very important to handle different input object types differently in a function. This problem will exercise your understanding of types, control-flow, dictionaries, and more.\n", "\n", ">We want to encode a sequence of Python objects as a single string. The following describes the encoding method that we want to use for each type of object. Each object's transcription in should be separated by `\" | \"`, and the result should be one large string. \n", "\n", "- If the object is an integer, convert it into a string by spelling out each digit in base-10 in this format:\n", "`142` $\\rightarrow$ `one-four-two`; `-12` $\\rightarrow$ `neg-one-two`. \n", "- If the object is a float, just append its integer part (obtained by rounding down) the same way and the string `\"and float\"`:\n", "`12.324` $\\rightarrow$ `one-two and float`. \n", "- If the object is a string, keep it as is.\n", "- If the object is of any other type, return `''`." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "``` Python\n", "# example behavior\n", ">>> s = concat_to_str([12,-14.23,\"hello\", True,\n", "... \"Aha\", 10.1, None, 5])\n", ">>> s\n", "'one-two | neg-one-four and float | hello | | Aha | one-zero and float | | five'\n", "```\n", "\n", "**Tips**: check out the `isinstance` function introduced [here](https://www.pythonlikeyoumeanit.com/Module2_EssentialsOfPython/Basic_Objects.html) for handling different types. Also, consider creating a helper function for the conversion from integer to our special-format string, since we have to do it twice. It's always good to extrapolate repeated tasks into functions. You'll also need to hard-code the conversion from each digit to its English spell-out. " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Solution\n", "Our solution is broken down into three simple functions. `int_to_str` is used to map signed integers to English words. `item_to_transcript` is capable of mapping an object of any type to its string representation, in accordance with the prescription made by the problem statement. Finally, `concat_to_str` orchestrates these two helper functions, looping over each object in our input list, mapping each object to its string representation, and joining these strings with `' | '`. \n", "\n", "```python\n", "def int_to_str(n):\n", " \"\"\" \n", " Takes an integer and formats it into a special string \n", " e.g. 142 -> \"one-four-two\"\n", " -12 -> \"neg-one-two\"\n", " \"\"\"\n", " mapping = {\"0\": \"zero\", \"1\": \"one\", \"2\": \"two\", \"3\": \"three\",\n", " \"4\": \"four\", \"5\": \"five\", \"6\": \"six\", \"7\": \"seven\",\n", " \"8\": \"eight\", \"9\": \"nine\", \"-\": \"neg\"}\n", " return \"-\".join(mapping[digit] for digit in str(n))\n", " \n", "def item_to_transcript(item):\n", " \"\"\" Any -> str \"\"\"\n", " if isinstance(item, bool): return ''\n", " if isinstance(item, int): return int_to_str(item)\n", " if isinstance(item, float): return int_to_str(int(item)) + \" and float\"\n", " if isinstance(item, str): return item\n", " return ''\n", "\n", "def concat_to_str(l):\n", " \"\"\" \n", " Maps a list of objects to their string \n", " representations concatenated together.\n", "\n", " Parameters\n", " ----------\n", " l: List[Any]\n", " Input list of objects\n", "\n", " Returns\n", " -------\n", " str\n", "\n", " Examples\n", " --------\n", " >>> concat_to_str([1, None, 'hi', 2.0])\n", " one | | hi | two and float\n", " \"\"\"\n", " return \" | \".join(item_to_transcript(item) for item in l)\n", "```\n", "\n", "We use the `str.join` function along with a generator comprehensions in a couple places in our solution. Recall that \n", "```python\n", "\"\".join(x for x in some_iterable_of_strings)\n", "```\n", "is equivalent to the long-form code:\n", "```python\n", "out = \"\"\n", "for x in some_iterable_of_strings:\n", " out += \"\" + x\n", "\n", "out = out.lstrip(\"\") # get rid of the extra leading \"\"\n", "``` \n", "\n", "`int_to_str` plays a clever trick to convert each integer, digit-by-digit, into its string form - it calls `str` on the integer. This converts the integer into a string, which is a [sequence](https://www.pythonlikeyoumeanit.com/Module2_EssentialsOfPython/SequenceTypes.html). This permits us to access each digit of the integer and even iterate over them:\n", "\n", "```python\n", "# casting an integer to a string makes its\n", "# sign and digits accessible via indexing/iteration\n", ">>> x = str(-123)\n", ">>> x\n", "'-123'\n", ">>> x[0]\n", "'-'\n", ">>> x[-1]\n", "'3'\n", "```\n", "Thus, in total `\"-\".join(mapping[digit] for digit in str(n))` is responsible for casting an integer to a string, iterating over each of its digits and mapping them to their corresponding word using the [dictionary](https://www.pythonlikeyoumeanit.com/Module2_EssentialsOfPython/DataStructures_II_Dictionaries.html) that we defined in the function.\n", "\n", "`item_to_transcript` it an especially slick function. First, let's make clear the fancy use of the inline syntax here. This function:\n", "```python\n", "def item_to_transcript(item):\n", " \"\"\" Any -> str \"\"\"\n", " if isinstance(item, bool): return ''\n", " if isinstance(item, int): return int_to_str(item)\n", " if isinstance(item, float): return int_to_str(int(item)) + \" and float\"\n", " if isinstance(item, str): return item\n", " return ''\n", "```\n", "is entirely equivalent to this function:\n", "```python\n", "def item_to_transcript_alt(item):\n", " \"\"\" Any -> str \"\"\"\n", " if isinstance(item, bool): \n", " return ''\n", " elif isinstance(item, int): \n", " return int_to_str(item)\n", " elif isinstance(item, float): \n", " return int_to_str(int(item)) + \" and float\"\n", " elif isinstance(item, str): \n", " return item\n", " else:\n", " return ''\n", "```\n", "The latter uses the familiar pattern of [if-elif-else](https://www.pythonlikeyoumeanit.com/Module2_EssentialsOfPython/ConditionalStatements.html) statements and makes for a completely satisfactory version of the function. See, however, that each of the [multiple return statements](https://www.pythonlikeyoumeanit.com/Module2_EssentialsOfPython/Functions.html#Multiple-return-Statements) in `item_to_transcript` guarantees the same logic, in that if a condition is meant a value will be returned by the function and none of its subsequent code can be visited. That is, if `item` is an integer the second if-condition will evaluate to `True` and `int_to_str(item)` will be returned, immediately expelling the point of execution from the body of the function.\n", "\n", "Ultimately, the preference of one function over the other is merely a matter of stylistic preference. You also have likely noted the peculiar in-line `if-return` expressions. These too are only stylistic choices; \n", "```python\n", "if isinstance(item, int): return int_to_str(item)\n", "```\n", "is no different from\n", "```python\n", "if isinstance(item, int): \n", " return int_to_str(item)\n", "```\n", "The use of in-line `if-return` expressions in `item_to_transcript` does a nice job emphasizing the dictionary-like mapping behavior of the function: the form of the code suits its functionality nicely. That being said, these should generally be used sparingly. Some may call this a \"cute\" trick. And it is. This code is cute. I write cute code.\n", "\n", "Finally, you may have noticed what looks like a redundancy in our code: our first `if` statement returns `''` if `item` is `True` or `False`, and our final line of code returns `''` if none of the preceding conditions were met (i.e. `item` is not a `bool`, `int`, `float`, or `str` type object). Why then did we not just merge our first `if` clause with this ultimate catch-all? The reason is that `True` and `False` are not only instances of the boolean type, they are also integers! `True` behaves like the integer `1` and `False` like `0`:\n", "\n", "```python\n", ">>> isinstance(True, int) and isinstance(True, bool)\n", "True\n", "\n", ">>> 3*True + True - False\n", "4\n", "```\n", "\n", "Thus, had we not taken care to check for booleans up front, `True` and `False` would have been mapped to `'one'` and `'zero'`, respectively, rather than `''`. This is a relatively subtle edge case to catch." ] } ], "metadata": { "jupytext": { "text_representation": { "extension": ".md", "format_name": "markdown", "format_version": "1.3", "jupytext_version": "1.13.6" } }, "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" } }, "nbformat": 4, "nbformat_minor": 4 }