{"id":"more-comprehensions-strings-objects","slug":"strings-and-objects","title":"Strings and your own objects","text":"A comprehension goes through anything a `for` loop can, and a string is a series of characters. So a comprehension over a string gets one character at a time. Here it picks the digits out of a code:\n\n```pycon\n>>> code = \"a1b2c3\"\n>>> [c for c in code if c in \"0123456789\"]\n['1', '2', '3']\n>>> [int(c) for c in code if c in \"0123456789\"]\n[1, 2, 3]\n>>> sum([int(c) for c in code if c in \"0123456789\"])\n6\n```\n\nWhatever it goes through, a comprehension in square brackets makes a list. When you want a string instead, hand the list of strings to the string method `join` from part 4. It makes one string of the pieces, with the string you call it on between each two of them:\n\n```pycon\n>>> digits = [c for c in \"a1b2c3\" if c in \"0123456789\"]\n>>> \"\".join(digits)\n'123'\n>>> \"+\".join(digits)\n'1+2+3'\n```\n\nThe pieces must be strings, though. The digits as numbers stop `join`:\n\n```python\nnumbers = [int(c) for c in \"a1b2c3\" if c in \"0123456789\"]\nprint(\"+\".join(numbers))\n```\n\n```crash\nTraceback (most recent call last):\n  File \"main.py\", line 2, in <module>\n    print(\"+\".join(numbers))\n          ~~~~~~~~^^^^^^^^^\nTypeError: sequence item 0: expected str instance, int found\n```\n\nItem 0, the first one, is an `int`, not a `str`. Keep the pieces as strings for `join`, or turn each number back into a string with a comprehension: `\"+\".join([str(n) for n in numbers])` gives `1+2+3`.\n\n### Word by word\n\nTo work on whole words instead of characters, `split` the string first. `split()` gives a list of the words, a comprehension changes each of them, and `join` puts them back together with a space between each two. The string method `capitalize` gives a word with its first letter in capitals and the rest in small letters, so it also fixes the all-capitals `LORD`:\n\n```python\ntitle = \"the LORD of the rings\"\nprint(\" \".join([word.capitalize() for word in title.split()]))\n```\n\n```output\nThe Lord Of The Rings\n```\n\nBack at the level of characters, an `if` and `else` in front of the `for`, as in the previous module, changes some characters and keeps the rest. This makes a file name out of a title:\n\n```python\ntitle = \"My Summer Photos\"\nfile_name = \"\".join([\"_\" if c == \" \" else c.lower() for c in title])\nprint(file_name + \".zip\")\n```\n\n```output\nmy_summer_photos.zip\n```\n\n### Objects of your own classes\n\nA list of objects works like any other list. The expression and the condition can use the objects' attributes and methods. The expression decides what goes into the new list: below, the first comprehension collects names, and the second one, with just `recipe` in front of the `for`, keeps the `Recipe` objects themselves:\n\n```python\nclass Recipe:\n    def __init__(self, name, minutes):\n        self.name = name\n        self.minutes = minutes\n\n\nrecipes = [Recipe(\"Pancakes\", 20), Recipe(\"Toast\", 5), Recipe(\"Lasagna\", 90), Recipe(\"Salad\", 10)]\nprint([recipe.name for recipe in recipes if recipe.minutes <= 15])\n\nquick = [recipe for recipe in recipes if recipe.minutes <= 15]\nfor recipe in quick:\n    recipe.name += \" (quick)\"\nprint([recipe.name for recipe in recipes])\n```\n\n```output\n['Toast', 'Salad']\n['Pancakes', 'Toast (quick)', 'Lasagna', 'Salad (quick)']\n```\n\n`quick` is a new list, but its items aren't new objects. As in part 9, a list holds references, so `quick` and `recipes` refer to the same two objects, `Toast` and `Salad`. The loop changes them through `quick`, and the change shows when the last line goes through `recipes`.\n\n### Your own iterable classes\n\nThe `for` inside a comprehension is the same `for` as in a loop, so it accepts the same things. That includes objects of a class with the methods `__iter__` and `__next__`, the iterable classes of part 10. A `Countdown` hands out the numbers from its start down to 1, and the comprehension gets them one by one, just like items of a list:\n\n```python\nclass Countdown:\n    def __init__(self, start):\n        self.start = start\n\n    def __iter__(self):\n        self.current = self.start\n        return self\n\n    def __next__(self):\n        if self.current == 0:\n            raise StopIteration\n        self.current -= 1\n        return self.current + 1\n\n\nprint([n * 10 for n in Countdown(4)])\n```\n\n```output\n[40, 30, 20, 10]\n```\n","task":"An acronym is made of the first letters of the words, in capitals. Fill in the blank so the program prints:\n\n```\nPNG\n```\n","kind":"fill","seed":"name = \"portable network graphics\"\nacronym = \"\".join([[[word[0]]].upper() for word in name.split()])\nprint(acronym)\n","solution":"name = \"portable network graphics\"\nacronym = \"\".join([word[0].upper() for word in name.split()])\nprint(acronym)\n","hints":["`name.split()` gives the list of words, and the comprehension goes through them one `word` at a time.","The first letter of a word is at index 0: the blank is `word[0]`."],"tests":[{"name":"Output","stdin":"","expect":"PNG","hidden":false}],"require":[],"forbid":[],"more":[{"task":"Write a function `double_letters(word)` that returns a new string in which every character of `word` is doubled. Use a list comprehension and `join`.\n\nFor example, `double_letters(\"hello\")` returns `\"hheelllloo\"`, so this call:\n\n```python\nprint(double_letters(\"hello\"))\n```\n\nprints:\n\n```\nhheelllloo\n```\n","kind":"code","seed":"def double_letters(word):\n    pass\n\n\nif __name__ == \"__main__\":\n    print(double_letters(\"hello\"))\n","solution":"def double_letters(word):\n    return \"\".join([c * 2 for c in word])\n\n\nif __name__ == \"__main__\":\n    print(double_letters(\"hello\"))\n","hints":["A comprehension over `word` goes through its characters. A character times 2 is that character twice: `\"e\" * 2` is `\"ee\"`.","`[c * 2 for c in word]` is a list of doubled characters. Join them with nothing between them: `\"\".join(...)`."],"tests":[{"name":"print(double_letters(\"hello\"))","stdin":"","call":"print(double_letters(\"hello\"))\n","expect":"hheelllloo","hidden":false},{"name":"double_letters(\"ab c\")","stdin":"","call":"double_letters(\"ab c\")\n","expect":"'aabb  cc'","hidden":true,"returns":true},{"name":"double_letters(\"\")","stdin":"","call":"double_letters(\"\")\n","expect":"''","hidden":true,"returns":true}],"require":[{"pattern":"\\[[^\\n]*\\bfor\\b","message":"Make the doubled characters with a list comprehension."},{"pattern":"\\.join\\s*\\(","message":"Put the pieces together into one string with `join`."}],"forbid":[]},{"task":"The program has a class `Song`, with a title and a length in seconds. Write a function `short_songs(songs, limit)` that returns a list of the titles of the songs that are shorter than `limit` seconds, in their original order. Use a list comprehension.\n\nFor example, this program:\n\n```python\nsongs = [Song(\"Intro\", 95), Song(\"Epic\", 610), Song(\"Hit\", 200)]\nprint(short_songs(songs, 300))\n```\n\nprints:\n\n```\n['Intro', 'Hit']\n```\n","kind":"code","seed":"class Song:\n    def __init__(self, title, seconds):\n        self.title = title\n        self.seconds = seconds\n\n\ndef short_songs(songs, limit):\n    pass\n\n\nif __name__ == \"__main__\":\n    songs = [Song(\"Intro\", 95), Song(\"Epic\", 610), Song(\"Hit\", 200)]\n    print(short_songs(songs, 300))\n","solution":"class Song:\n    def __init__(self, title, seconds):\n        self.title = title\n        self.seconds = seconds\n\n\ndef short_songs(songs, limit):\n    return [song.title for song in songs if song.seconds < limit]\n\n\nif __name__ == \"__main__\":\n    songs = [Song(\"Intro\", 95), Song(\"Epic\", 610), Song(\"Hit\", 200)]\n    print(short_songs(songs, 300))\n","hints":["The new list should hold titles, so the expression is the song's `title` attribute.","Filter with the `seconds` attribute: `if song.seconds < limit`.","Put it together: `[song.title for song in songs if ...]`."],"tests":[{"name":"Test 1","stdin":"","call":"songs = [Song(\"Intro\", 95), Song(\"Epic\", 610), Song(\"Hit\", 200)]\nprint(short_songs(songs, 300))\n","expect":"['Intro', 'Hit']","hidden":false},{"name":"short_songs([Song(\"A\", 300), Song(\"B\", 299), Song(\"C\", 30)], 300)","stdin":"","call":"short_songs([Song(\"A\", 300), Song(\"B\", 299), Song(\"C\", 30)], 300)\n","expect":"['B', 'C']","hidden":true,"returns":true},{"name":"short_songs([Song(\"Long\", 900)], 60)","stdin":"","call":"short_songs([Song(\"Long\", 900)], 60)\n","expect":"[]","hidden":true,"returns":true}],"require":[{"pattern":"\\[[^\\n]*\\bfor\\b","message":"Build the list of titles with a list comprehension."}],"forbid":[]}]}