diff --git a/concepts/itertools/.meta/config.json b/concepts/itertools/.meta/config.json index 9b9e8da5a9..3bf2b514cc 100644 --- a/concepts/itertools/.meta/config.json +++ b/concepts/itertools/.meta/config.json @@ -1,5 +1,5 @@ { "blurb": "TODO: add blurb for this concept", - "authors": ["bethanyg", "cmccandless"], + "authors": ["bethanyg", "meatball133"], "contributors": [] } diff --git a/concepts/itertools/about.md b/concepts/itertools/about.md index c628150d56..8a3ae2bad8 100644 --- a/concepts/itertools/about.md +++ b/concepts/itertools/about.md @@ -1,2 +1,406 @@ -#TODO: Add about for this concept. +# About +[`itertools`][itertools] is a module in the Python standard library that provides a number of functions that create iterators for more efficient looping. + +[Iterators][iterators] are objects that represent a stream of data. + +An example is the "stream" of data when you iterate over a `list` in a `for in :` loop. + + +Many functions in `itertools` are very useful for looping/iterating over data streams in various ways. + +These functions often enhance the readability and/or maintainability of the code. + + +This concept will cover a selection of these functions and how to use them: + + +- `chain()` +- `chain.from_iterable()` +- `compress()` +- `islice()` +- `pairwise()` +- `zip_longest()` +- `product()` +- `permutations()` +- `combinations()` +- `count()` +- `cycle()` + +There are more functions in the itertools module, like: + +- `accumulate()` +-`combinations_with_replacement()` +- `groupby()` + +- `repeat()` +- `starmap()` + +- `takewhile()` +- `dropwhile()` +- `filterfalse()` + +These functions will be covered in a later concept. + +`count()`, `cycle()`, and `repeat()` are categorized as infinite iterators. + +These iterators will never terminate and will keep looping forever. + +## Iterators terminating on the shortest input sequence + +### Chain() + +`chain(iterable1, iterable2...)` creates an iterator of values from the iterables in the order they are given. + +```python +>>> import itertools +>>> for number in itertools.chain([1, 2, 3], [4, 5, 6], [7, 8, 9]): +... print(number, end=' ') +... +1 2 3 4 5 6 7 8 9 +``` + +Since `chain()` takes iterables as arguments, so can it take it for example take: `set`, `tuple`, `list`, `str`, and more. +You can give iterables of different types at the same time. + +```python +>>> import itertools +>>> for number in itertools.chain([1, 2, 3], (4, 5, 6), {7, 8, 9}, "abc"): +... print(number, end=' ') +... +1 2 3 4 5 6 7 8 9 a b c +``` + +Chain can also be used to concate a different amount of iterables to a list or tuple by using the `list()` or `tuple()` function. + +Using `list()`: + +```python +>>> import itertools +>>> list(itertools.chain([1, 2, 3], [4, 5, 6], [7, 8, 9])) +[1, 2, 3, 4, 5, 6, 7, 8, 9] +``` + +Using `tuple()`: + +```python +>>> import itertools +>>> tuple(itertools.chain([1, 2, 3], [4, 5, 6], [7, 8, 9])) +(1, 2, 3, 4, 5, 6, 7, 8, 9) +``` + +### chain.from_iterable() + +`chain.from_iterable()` works like `chain` but takes a single nested iterable. +Then the method unpack that iterable into individual iterables. + +```python +>>> import itertools +>>> for number in itertools.chain.from_iterable( + [ + [1, 2, 3], + [4, 5, 6], + [7, 8, 9] + ]): +... print(number, end=' ') +... +1 2 3 4 5 6 7 8 9 +``` + +### Compress() + +`compress(iterable, selectors)` creates an iterator from the input iterable where the corresponding selector is `True`. +The selector can be `True`/`False` or integers, where 0 is `False` and 1 is `True`. +You can read more about the behavior when [numbers act like booleans][numbers-as-boolean-values]. + +```python +>>> import itertools +>>> for letter in itertools.compress("Exercism", [1, 0, 0, 1, 1, 0, 1, 1]): +... print(letter, end=' ') +... +E r c s m +``` + +With `True`/`False`: + +```python +>>> import itertools +>>> for letter in itertools.compress("Exercism", [True, False, False, True, True, False, True, True]): +... print(letter, end=' ') +... +E r c s m +``` + +### Islice() + +`islice(iterable, start, , )` creates a new iterator from the slice (from the start index to the stop index with a step size of step). + +```python +>>> import itertools +>>> for letter in itertools.islice("Exercism", 2, 5, 2): +... print(letter, end=' ') +... +e c +``` + +### Pairwise() + +```exercism/note +`Pairwise()` requires Python 3.10+. +If you are using the online editor then you don't need to worry about this. +``` + +`Pairwise(iterable)` was introduced in Python 3.10 and returns an iterator of overlapping pairs of values from the input iterable. + +```python +>>> import itertools +>>> for pair in itertools.pairwise("Exercism"): +... print(pair, end=' ') +... +('E', 'x') ('x', 'e') ('e', 'r') ('r', 'c') ('c', 'i') ('i', 's') ('s', 'm') +``` + +### Tee() + +Talk with Bethany about + +### Zip_longest() + +#### Explaining zip + +```exercism/caution +Pythons `zip()` function should not be confused with the zip compression format. +``` + +`zip()` is a built in function and is not apart of the `itertools` module. +It takes any number of iterables and returns an iterator of tuples. +Where the i-th tuple contains the i-th element from each of the argument iterables. +For example, the first tuple will contain the first element from each iterable, the second tuple will contain the second element from each iterable, and so on until the shortest iterable is exhausted. + +```python +>>> zipped = zip(['x', 'y', 'z'], [1, 2, 3], [True, False, True]) +>>> list(zipped) +[('x', 1, True),('y', 2, False), ('z', 3, True)] +``` + +If the iterables are not the same length, then the iterator will stop when the shortest iterable is exhausted. + +```python +>>> zipped = zip(['x', 'y', 'z'], [1, 2, 3, 4], [True, False]) +>>> list(zipped) +[('x', 1, True),('y', 2, False)] +``` + +#### Explaining zip_longest + +`zip_longest(iterator, )` is a function from the `itertools` module. +Unlike `zip()`, it will not stop when the shortest iterable is exhausted. +If the iterables are not the same length, `fillvalue` will be used to pad missing values. +By the default the `fillvalue` is `None`. + +```python +>>> import itertools +>>> zipped = itertools.zip_longest(['x', 'y', 'z'], [1, 2, 3, 4], [True, False]) +>>> list(zipped) +[('x', 1, True),('y', 2, False), ('z', 3, None), (None, 4, None)] +``` + +An example where a `fillvalue` is given: + +```python +>>> import itertools +>>> zipped = itertools.zip_longest(['x', 'y', 'z'], [1, 2, 3, 4], [True, False], fillvalue='fill') +>>> list(zipped) +[('x', 1, True),('y', 2, False), ('z', 3, 'fill'), ('fill', 4, 'fill')] +``` + +## Combinatoric iterators + +### Product() + +`product(iterable1, iterable2..., )` creates an iterator of tuples where the i-th tuple contains the i-th element from each of the argument iterables. +The repeat keyword argument can be used to specify the number of times the input iterables are repeated. +By default the repeat keyword argument is 1. + +```python +>>> import itertools +>>> for product in itertools.product("ABCD", repeat=1): +... print(product, end=' ') +... +('A',) ('B',) ('C',) ('D',) +``` + +Giving a repeat value of 2: + +```python +>>> import itertools +>>> for product in itertools.product("ABCD", repeat=2): +... print(product, end=' ') +... +('A', 'A') ('A', 'B') ('A', 'C') ('A', 'D') ('B', 'A') ('B', 'B') ('B', 'C') ('B', 'D') ('C', 'A') ('C', 'B') ('C', 'C') ('C', 'D') ('D', 'A') ('D', 'B') ('D', 'C') ('D', 'D') +``` + +The last one here can be seen as doing a nested for loop. +When you increase the repeat value the number of iterations increases exponentially. +The example above is a n\*\*2 iteration. + +```python +>>> import itertools +>>> for letter1 in "ABCD": +... for letter2 in "ABCD": +... print((letter1, letter2), end=' ') +... +('A', 'A') ('A', 'B') ('A', 'C') ('A', 'D') ('B', 'A') ('B', 'B') ('B', 'C') ('B', 'D') ('C', 'A') ('C', 'B') ('C', 'C') ('C', 'D') ('D', 'A') ('D', 'B') ('D', 'C') ('D', 'D') +``` + +You can also give it multiple iterables. + +```python +>>> import itertools +>>> for product in itertools.product("ABCD", "xy" repeat=1): +... print(product, end=' ') +... +('A', 'x') ('A', 'y') ('B', 'x') ('B', 'y') ('C', 'x') ('C', 'y') ('D', 'x') ('D', 'y') +``` + +Here is an example of doing it without `product()`. +It looks similar to the last example but since we have two iterables we need to nest the for loops. +Even though the product is given repeat=1. +The reason to why it is only 2 for loops earlier was because we only had one iterable. +If we had two iterables and gave it repeat=2 we would need 4 for loops. +Since 2 \* 2 = 4. + +```python +>>> for letter1 in "ABCD": +... for letter2 in "xy": +... print((letter1, letter2), end=' ') +... +('A', 'x') ('A', 'y') ('B', 'x') ('B', 'y') ('C', 'x') ('C', 'y') ('D', 'x') ('D', 'y') +``` + +### Permutations() + +`permutations(iterable, )` creates an iterator of tuples. +It works like `product()` but it doesn't repeat values from a specific position from the iterable and can only take one iterable. +The **r** keyword argument can be used to specify the number of times the input iterables are repeated. +By default the **r** keyword argument is None. +If **r** is None then the length of the iterable is used. + +```python +>>> import itertools +>>> for permutation in itertools.permutations("ABCD", repeat=1): +... print(permutation, end=' ') +... +('A',) ('B',) ('C',) ('D',) +``` + +```python +>>> import itertools +>>> for permutation in itertools.permutations("ABCD", repeat=2): +... print(permutation, end=' ') +... +('A', 'B') ('A', 'C') ('A', 'D') ('B', 'A') ('B', 'C') ('B', 'D') ('C', 'A') ('C', 'B') ('C', 'D') ('D', 'A') ('D', 'B') ('D', 'C') +``` + +### Combinations() + +`combinations(iterable, r)` finds all the possible combinations of the given iterable. +The **r** keyword argument is used to specify the length of the tuples generated. + +```python +>>> import itertools +>>> for combination in itertools.combinations("ABCD", 2): +... print(combination, end=' ') +... +('A', 'B') ('A', 'C') ('A', 'D') ('B', 'C') ('B', 'D') ('C', 'D') +``` + +### Combinations_with_replacement() + +`combinations_with_replacement(iterable, r)` finds all the possible combinations of the given iterable. +The **r** keyword argument is used to specify the length of the tuples generated. +The difference between this and `combinations()` is that it can repeat values. +That means that if you have "AB" and you want to find all the combinations of length 2 you will get `("A", "A"), ("A", "B"), ("B", "B")`. +While with `combinations()` you would only get `("A", "B")`. + +```python +>>> import itertools +>>> for combination in itertools.combinations_with_replacement("ABCD", 2): +... print(combination, end=' ') +... +('A', 'A') ('A', 'B') ('A', 'C') ('A', 'D') ('B', 'B') ('B', 'C') ('B', 'D') ('C', 'C') ('C', 'D') ('D', 'D') +``` + +## Infinite iterators + +Most of iterator from the `itertools` module get exhausted after a time. +But there are some that are infinite, these are known as infinite iterators. +These iterators will will keep producing values until you tell them to stop. + +```exercism/note +To avoid infinite loops, you can use `break` to end a loop. +``` + +### Count() + +`count(start, )` produces all values from the start value to infinity. +Count also has an optional step parameter, which will produce values with a step size other than 1. + +```python +>>> import itertools +>>> for number in itertools.count(5, 2): +... if number > 20: +... break +... else: +... print(number, end=' ') +... +5 7 9 11 13 15 17 19 +``` + +Giving `count()` a negative step size will produces values in a descending order. + +```python +>>> import itertools +>>> for number in itertools.count(5, -2): +... if number < -20: +... break +... else: +... print(number, end=' ') +... +5 3 1 -1 -3 -5 -7 -9 -11 -13 -15 -17 -19 +``` + +### Cycle() + +`cycle(iterable)` produces all values from the iterable in an infinite loop. +A `list`, `tuple`, `string`, `dict` or any other iterable can be used. + +```python +>>> import itertools +>>> number = 0 +>>> for letter in itertools.cycle("ABC"): +... if number == 10: +... break +... else: +... print(letter, end=' ') +... number += 1 +... +A B C A B C A B C A +``` + +### Repeat() + +`repeat(object, )` produces the same value in an infinite loop. +Although if the optional times parameter is given, the value will produces that many times. +Meaning that it is not an infinite loop if that parameter is given. + +```python +>>> import itertools +>>> for number in itertools.repeat(5, 3): +... print(number, end=' ') +... +5 5 5 +``` + +[itertools]: https://docs.python.org/3/library/itertools.html +[numbers-as-boolean-values]: https://realpython.com/python-boolean/#numbers-as-boolean-values diff --git a/concepts/itertools/links.json b/concepts/itertools/links.json index eb5fb7c38a..f925725ffb 100644 --- a/concepts/itertools/links.json +++ b/concepts/itertools/links.json @@ -1,15 +1,15 @@ [ { - "url": "http://example.com/", - "description": "TODO: add new link (above) and write a short description here of the resource." + "url": "https://docs.python.org/3/library/itertools.html", + "description": "Official Python documentation for the itertools module." }, { - "url": "http://example.com/", - "description": "TODO: add new link (above) and write a short description here of the resource." + "url": "https://realpython.com/python-itertools/", + "description": "Real python, itertools in python" }, { - "url": "http://example.com/", - "description": "TODO: add new link (above) and write a short description here of the resource." + "url": "https://www.geeksforgeeks.org/python-itertools/", + "description": "Geeks of geeks, python itertools" }, { "url": "http://example.com/", diff --git a/config.json b/config.json index b2222755eb..1546fbfd9f 100644 --- a/config.json +++ b/config.json @@ -136,6 +136,14 @@ "prerequisites": ["bools", "loops", "conditionals", "numbers"], "status": "beta" }, + { + "slug": "ice-cream-stand", + "name": "Ice Cream Stand", + "uuid": "121388ab-d86c-498d-928c-1fe536583e97", + "concepts": ["itertools"], + "prerequisites": ["tuples"], + "status": "wip" + }, { "slug": "inventory-management", "name": "Inventory Management", diff --git a/exercises/concept/ice-cream-stand/.docs/hints.md b/exercises/concept/ice-cream-stand/.docs/hints.md new file mode 100644 index 0000000000..4b90eab35e --- /dev/null +++ b/exercises/concept/ice-cream-stand/.docs/hints.md @@ -0,0 +1,19 @@ +# Hints + +## General + +- All of the problems is solvable with different methods from the `itertools` module. + +## 1. ice_cream_combinations + +- The `itertools.combinations(iterable, size)` method can be used to generate all combinations of a an iterable like a `tuple` or `list`. + +## 2. sprinkles + +- The `itertools.compress(iterable, selector)` method can be used to filter an iterable based on a boolean selector. + +## 3. fill_out_ice_cream_menu + +- You can use `zip()` to combine an arbitrary number of iterables into a `tuple` of `tuples`. +- The `itertools.zip_longest(iterable, fillvalue)` method can be used to combine iterables of different lengths. +- You can give a value to the `fillvalue` parameter to fill in missing values. diff --git a/exercises/concept/ice-cream-stand/.docs/instructions.md b/exercises/concept/ice-cream-stand/.docs/instructions.md new file mode 100644 index 0000000000..8a469bf1c0 --- /dev/null +++ b/exercises/concept/ice-cream-stand/.docs/instructions.md @@ -0,0 +1,76 @@ +# Instructions + +An ice cream stand in the neighborhood does a lot of manual work. +You have offered to help them with a program that can automate some of the work. + +```exercism/note +This exercise could be solved with a lot of different approaches. +However, we would like you to practice using methods from the `itertools` module. +``` + +## 1. All flavors combinations + +The ice cream stand wants to make an advertisement for how many different ice creams they can make. +They therefore want a program that can generate all the combinations of flavors they can make. +Each ice cream has a different amount of scoops of flavors but you can't use the same flavor more than once. + +Implement a function `ice_cream_combinations()` that takes a `tuple` with an arbitrary number of flavors and an `int` which says how many scoops. +The function should then `return` all combinations in the form of a `tuple`. + +```python +>>> flavors = ['vanilla', 'chocolate', 'strawberry'] +>>> scoops = 2 +>>> ice_cream_combinations(flavors, scoops) +(('vanilla', 'chocolate'), ('vanilla', 'strawberry'), ('chocolate', 'strawberry')) +``` + +## 2. Sprinkles + +They also want a program to optimize the process of adding sprinkles to the ice cream. +Currently they have a `list` of all ice cream order numbers and another `list` with `booleans` that say if the ice cream should have sprinkles or not. +The order numbers and the `booleans` are in the same order. + +The ice cream stand has a machine that takes a `list` of orders that should have sprinkles and adds sprinkles to them. +Therefore they want a program that takes away all the orders that should not have sprinkles. + +Implement a function `sprinkles()` that takes a `list` of order numbers and a `list` of `booleans` and returns a `list` of order numbers that should have sprinkles. + +```python +>>> ice_creams = ['ice_cream_1', 'ice_cream_2', 'ice_cream_3'] +>>> selector = [0, 1, 0] +>>> sprinkles(ice_creams, selector) +['ice_cream_2'] +``` + +## 3. Fill out ice cream menu + +Currently the ice cream has to manually write down the ice cream menu. +Since they often make new ice creams they want a program that can generate the menu for them. + +The menu is built up like this: + +| Flavors | Toping | Sprinkles | +| ---------- | --------- | --------- | +| Strawberry | Cherry | Licorice | +| Chocolate | Raspberry | Caramel | +| Mint | Blueberry | None | +| Vanilla | None | None | + +The ice cream stand has a `tuple` with all the ice cream flavors, a `tuple` with all the toppings and a `tuple` with all the sprinkles. +They have set it up so the `tuple` of ice cream flavors, toppings and sprinkles are in the same order. + +They want a program that takes **i**th index of the `tuple` of ice cream flavors, toppings and sprinkles and returns a `tuple` with the ice cream menu. + +All ice creams flavors doesn't have to have a toping or sprinkles. +If an ice cream doesn't have a toping or sprinkles the value should be `"None"`. + +Implement a function `fill_out_ice_cream_menu()` that accepts a `tuple` with ice cream flavors, a `tuple` with toppings, and a `tuple` sprinkles. +The function should `return` a `list` of `tuples` with the ice cream menu. + +```python +>>> flavors = ('vanilla', 'chocolate', 'strawberry') +>>> toppings = ('cherry', 'raspberry') +>>> sprinkles = ('licorice') +>>> fill_out_ice_cream_menu(flavors, toppings, sprinkles) +[('vanilla', 'cherry', 'licorice'), ('chocolate', 'raspberry', 'None'), ('strawberry', 'None', 'None')] +``` diff --git a/exercises/concept/ice-cream-stand/.docs/introduction.md b/exercises/concept/ice-cream-stand/.docs/introduction.md new file mode 100644 index 0000000000..e010c07576 --- /dev/null +++ b/exercises/concept/ice-cream-stand/.docs/introduction.md @@ -0,0 +1,362 @@ +# Unpacking and Multiple Assignment + +Unpacking refers to the act of extracting the elements of a collection, such as a `list`, `tuple`, or `dict`, using iteration. +Unpacked values can then be assigned to variables within the same statement, which is commonly referred to as [Multiple assignment][multiple assignment]. + +The special operators `*` and `**` are often used in unpacking contexts and with multiple assignment. + +```exercism/caution +`*` and `**` should not be confused with `*` and `**`. While `*` and `**` are used for multiplication and exponentiation respectively, `*` and `**` are used as packing and unpacking operators. +``` + +## Multiple assignment + +In multiple assignment, the number of variables on the left side of the assignment operator (`=`) must match the number of values on the right side. +To separate the values, use a comma `,`: + +```python +>>> a, b = 1, 2 +>>> a +1 +``` + +If the multiple assignment gets an incorrect number of variables for the values given, a `ValueError` will be thrown: + +```python +>>> x, y, z = 1, 2 + +ValueError: too many values to unpack (expected 3, got 2) +``` + +Multiple assignment is not limited to one data type: + +```python +>>> x, y, z = 1, "Hello", True +>>> x +1 + +>>> y +'Hello' + +>>> z +True +``` + +Multiple assignment can be used to swap elements in `lists`. +This practice is pretty common in [sorting algorithms][sorting algorithms]. +For example: + +```python +>>> numbers = [1, 2] +>>> numbers[0], numbers[1] = numbers[1], numbers[0] +>>> numbers +[2, 1] +``` + +Since `tuples` are immutable, you can't swap elements in a `tuple`. + +## Unpacking + +```exercism/note +The examples below use `lists` but the same concepts apply to `tuples`. +``` + +In Python, it is possible to [unpack the elements of `list`/`tuple`/`dictionary`][unpacking] into distinct variables. +Since values appear within `lists`/`tuples` in a specific order, they are unpacked into variables in the same order: + +```python +>>> fruits = ["apple", "banana", "cherry"] +>>> x, y, z = fruits +>>> x +"apple" +``` + +If there are values that are not needed then you can use `_` to flag them: + +```python +>>> fruits = ["apple", "banana", "cherry"] +>>> _, _, z = fruits +>>> z +"cherry" +``` + +### Deep unpacking + +Unpacking and assigning values from a `list`/`tuple` inside of a `list` or `tuple` (_also known as nested lists/tuples_), works in the same way a shallow unpacking does, but often needs qualifiers to clarify the values context or position: + +```python +>>> fruits_vegetables = [["apple", "banana"], ["carrot", "potato"]] +>>> [[a, b], [c, d]] = fruits_vegetables +>>> a +"apple" + +>>> d +"potato" +``` + +You can also deeply unpack just a portion of a nested `list`/`tuple`: + +```python +>>> fruits_vegetables = [["apple", "banana"], ["carrot", "potato"]] +>>> [a, [c, d]] = fruits_vegetables +>>> a +["apple", "banana"] + +>>> c +"carrot" +``` + +If the unpacking has variables with incorrect placement and/or an incorrect number of values, you will get a `ValueError`: + +```python +>>> fruits_vegetables = [["apple", "banana"], ["carrot", "potato"]] +>>> [[a, b], [d]] = fruits_vegetables + +ValueError: too many values to unpack (expected 1) +``` + +### Unpacking a list/tuple with `*` + +When [unpacking a `list`/`tuple`][packing and unpacking] you can use the `*` operator to capture the "leftover" values. +This is clearer than slicing the `list`/`tuple` (_which in some situations is less readable_). +For example, we can extract the first element and then assign the remaining values into a new `list` without the first element: + +```python +>>> fruits = ["apple", "banana", "cherry", "orange", "kiwi", "melon", "mango"] +>>> x, *last = fruits +>>> x +"apple" + +>>> last +["banana", "cherry", "orange", "kiwi", "melon", "mango"] +``` + +We can also extract the values at the beginning and end of the `list` while grouping all the values in the middle: + +```python +>>> fruits = ["apple", "banana", "cherry", "orange", "kiwi", "melon", "mango"] +>>> x, *middle, y, z = fruits +>>> y +"melon" + +>>> middle +["banana", "cherry", "orange", "kiwi"] +``` + +We can also use `*` in deep unpacking: + +```python +>>> fruits_vegetables = [["apple", "banana", "melon"], ["carrot", "potato", "tomato"]] +>>> [[a, *rest], b] = fruits_vegetables +>>> a +"apple" + +>>> rest +["banana", "melon"] +``` + +### Unpacking a dictionary + +[Unpacking a dictionary][packing and unpacking] is a bit different than unpacking a `list`/`tuple`. +Iteration over dictionaries defaults to the **keys**. +So when unpacking a `dict`, you can only unpack the **keys** and not the **values**: + +```python +>>> fruits_inventory = {"apple": 6, "banana": 2, "cherry": 3} +>>> x, y, z = fruits_inventory +>>> x +"apple" +``` + +If you want to unpack the values then you can use the `values()` method: + +```python +>>> fruits_inventory = {"apple": 6, "banana": 2, "cherry": 3} +>>> x, y, z = fruits_inventory.values() +>>> x +6 +``` + +If both **keys** and **values** are needed, use the `items()` method. +Using `items()` will generate tuples with **key-value** pairs. +This is because of [`dict.items()` generates an iterable with key-value `tuples`][items]. + +```python +>>> fruits_inventory = {"apple": 6, "banana": 2, "cherry": 3} +>>> x, y, z = fruits_inventory.items() +>>> x +("apple", 6) +``` + +## Packing + +[Packing][packing and unpacking] is the ability to group multiple values into one `list` that is assigned to a variable. +This is useful when you want to _unpack_ values, make changes, and then _pack_ the results back into a variable. +It also makes it possible to perform merges on 2 or more `lists`/`tuples`/`dicts`. + +### Packing a list/tuple with `*` + +Packing a `list`/`tuple` can be done using the `*` operator. +This will pack all the values into a `list`/`tuple`. + +```python +>>> fruits = ("apple", "banana", "cherry") +>>> more_fruits = ["orange", "kiwi", "melon", "mango"] + +# fruits and more_fruits are unpacked and then their elements are packed into combined_fruits +>>> combined_fruits = *fruits, *more_fruits + +# If there is no * on to the left of the "=" the result is a tuple +>>> combined_fruits +("apple", "banana", "cherry", "orange", "kiwi", "melon", "mango") + +# If the * operator is used on the left side of "=" the result is a list +>>> *combined_fruits_too, = *fruits, *more_fruits +>>> combined_fruits_too +['apple', 'banana', 'cherry', 'orange', 'kiwi', 'melon', 'mango'] +``` + +### Packing a dictionary with `**` + +Packing a dictionary is done by using the `**` operator. +This will pack all **key**-**value** pairs from one dictionary into another dictionary, or combine two dictionaries together. + +```python +>>> fruits_inventory = {"apple": 6, "banana": 2, "cherry": 3} +>>> more_fruits_inventory = {"orange": 4, "kiwi": 1, "melon": 2, "mango": 3} + +# fruits_inventory and more_fruits_inventory are unpacked into key-values pairs and combined. +>>> combined_fruits_inventory = {**fruits_inventory, **more_fruits_inventory} + +# then the pairs are packed into combined_fruits_inventory +>>> combined_fruits_inventory +{"apple": 6, "banana": 2, "cherry": 3, "orange": 4, "kiwi": 1, "melon": 2, "mango": 3} +``` + +## Usage of `*` and `**` with functions + +### Packing with function parameters + +When you create a function that accepts an arbitrary number of arguments, you can use [`*args` or `**kwargs`][args and kwargs] in the function definition. +`*args` is used to pack an arbitrary number of positional (non-keyworded) arguments and +`**kwargs` is used to pack an arbitrary number of keyword arguments. + +Usage of `*args`: + +```python +# This function is defined to take any number of positional arguments + +>>> def my_function(*args): +... print(args) + +# Arguments given to the function are packed into a tuple + +>>> my_function(1, 2, 3) +(1, 2, 3) + +>>> my_function("Hello") +("Hello") + +>>> my_function(1, 2, 3, "Hello", "Mars") +(1, 2, 3, "Hello", "Mars") +``` + +Usage of `**kwargs`: + +```python +# This function is defined to take any number of keyword arguments + +>>> def my_function(**kwargs): +... print(kwargs) + +# Arguments given to the function are packed into a dictionary + +>>> my_function(a=1, b=2, c=3) +{"a": 1, "b": 2, "c": 3} +``` + +`*args` and `**kwargs` can also be used in combination with one another: + +```python +>>> def my_function(*args, **kwargs): +... print(sum(args)) +... for key, value in kwargs.items(): +... print(str(key) + " = " + str(value)) + +>>> my_function(1, 2, 3, a=1, b=2, c=3) +6 +a = 1 +b = 2 +c = 3 +``` + +You can also write parameters before `*args` to allow for specific positional arguments. +Individual keyword arguments then have to appear before `**kwargs`. + +```exercism/caution +[Arguments have to be structured](https://www.python-engineer.com/courses/advancedpython/18-function-arguments/) like this: + +`def my_function(, *args, , **kwargs)` + +If you don't follow this order then you will get an error. +``` + +```python +>>> def my_function(a, b, *args): +... print(a) +... print(b) +... print(args) + +>>> my_function(1, 2, 3, 4, 5) +1 +2 +(3, 4, 5) +``` + +Writing arguments in an incorrect order will result in an error: + +```python +>>>def my_function(*args, a, b): +... print(args) + +>>>my_function(1, 2, 3, 4, 5) +Traceback (most recent call last): + File "c:\something.py", line 3, in + my_function(1, 2, 3, 4, 5) +TypeError: my_function() missing 2 required keyword-only arguments: 'a' and 'b' +``` + +### Unpacking into function calls + +You can use `*` to unpack a `list`/`tuple` of arguments into a function call. +This is very useful for functions that don't accept an `iterable`: + +```python +>>> def my_function(a, b, c): +... print(c) +... print(b) +... print(a) + +numbers = [1, 2, 3] +>>> my_function(*numbers) +3 +2 +1 +``` + +Using `*` unpacking with the `zip()` function is another common use case. +Since `zip()` takes multiple iterables and returns a `list` of `tuples` with the values from each `iterable` grouped: + +```python +>>> values = (['x', 'y', 'z'], [1, 2, 3], [True, False, True]) +>>> a, *rest = zip(*values) +>>> rest +[('y', 2, False), ('z', 3, True)] +``` + +[args and kwargs]: https://www.geeksforgeeks.org/args-kwargs-python/ +[items]: https://www.geeksforgeeks.org/python-dictionary-items-method/ +[multiple assignment]: https://www.geeksforgeeks.org/assigning-multiple-variables-in-one-line-in-python/ +[packing and unpacking]: https://www.geeksforgeeks.org/packing-and-unpacking-arguments-in-python/ +[sorting algorithms]: https://realpython.com/sorting-algorithms-python/ +[unpacking]: https://www.geeksforgeeks.org/unpacking-arguments-in-python/?ref=rp diff --git a/exercises/concept/ice-cream-stand/.meta/config.json b/exercises/concept/ice-cream-stand/.meta/config.json new file mode 100644 index 0000000000..d01c09fed3 --- /dev/null +++ b/exercises/concept/ice-cream-stand/.meta/config.json @@ -0,0 +1,19 @@ +{ + "authors": [ + "meatball133", + "BethanyG" + ], + "files": { + "solution": [ + "ice_cream_stand.py" + ], + "test": [ + "ice_cream_stand_test.py" + ], + "exemplar": [ + ".meta/exemplar.py" + ] + }, + "icon": "custom-set", + "blurb": "Learn about itertools while helping the local ice cream stand" +} diff --git a/exercises/concept/ice-cream-stand/.meta/design.md b/exercises/concept/ice-cream-stand/.meta/design.md new file mode 100644 index 0000000000..375666d767 --- /dev/null +++ b/exercises/concept/ice-cream-stand/.meta/design.md @@ -0,0 +1,79 @@ +# Design + +## Goal + +The goal of the concept exercise described in this issue is to teach understanding/use of the itertools module in Python. + +## Learning objectives + +Learn more about iteration tools the Python Standard Library provides through the itertools module. + +Build and understanding of and use the following functions from the module, as well as practicing some of the recipes included: + +At least one of the infinite itertators: `count()`, `cycle()`, or `repeat()` + +- `accumulate()` +- `product()` +- `chain() & chain.from_iterable()` +- `groupby()` +- `islice()` +- `zip_longest() and the zip() built-in` +- `permutations()` +- `combinations()` + +## Concepts + +- iteration +- iterators +- itertools + +## Topics that are Out of scope + +- `classes` & `class customization` beyond the use of the `itertools` methods. +- `class-inheritance` beyond what is needed to customize iteration using `itertools` +- `comprehensions` beyond what is needed to work with itertools +- `comprehensions` in `lambdas` +- coroutines +- `decorators` beyond what is needed to work with `itertools` +- functions and higher-order functions beyond what might be needed to work with itertools +- `functools` and related `map`, `filter()` and `functools.reduce()`(they have their own exercise which is a prerequisite to this one) +- `generators` beyond what might be needed to work with itertools (they have their own exercise which is a prerequisite to this one) +- `lambdas` beyond what might be needed to work with `itertools` +- using an assignment expression or "walrus" operator (:=) +- class decorators +- enums + +## Prerequisites + +- `basics` +- `booleans` +- `comparisons` +- `rich-comparisons` +- **dicts** +- **dict-methods** +- **functions** +- **functional tools** +- _generators_ +- **higher-order functions** +- **Identity methods is and is not** +- `iteration` +- `lists` +- `list-methods` +- `loops` +- `numbers` +- `sequences` +- **sets** +- `strings` +- `string-methods` +- `tuples` + +## Representer + +This exercise does not require any specific logic to be added to the [representer][representer] + +## Analyzer + +This exercise does not require any specific logic to be added to the [analyzer][analyzer]. + +[analyzer]: https://github.com/exercism/python-analyzer +[representer]: https://github.com/exercism/python-representer diff --git a/exercises/concept/ice-cream-stand/.meta/exemplar.py b/exercises/concept/ice-cream-stand/.meta/exemplar.py new file mode 100644 index 0000000000..38350415c5 --- /dev/null +++ b/exercises/concept/ice-cream-stand/.meta/exemplar.py @@ -0,0 +1,36 @@ +"""Functions for ice cream stand.""" + +import itertools + +def ice_cream_combinations(flavors, scoops): + """Return a tuple of all possible combinations without repetition. + + :param flavors: list - arbitrary number of flavors. + :param scoops: int - number of scoops. + :return: tuple - tuple of all possible combinations. + """ + + return tuple(itertools.combinations(flavors, scoops)) + + +def sprinkles(ice_creams, selector): + """Get the ice cream with sprinkles. + + :param ice_creams: list - ice_cream_orders. + :param selector: list - which ice creams that needs sprinkles. + :return: list - ice creams that needs sprinkles. + """ + return list(itertools.compress(ice_creams, selector)) + + +def fill_out_ice_cream_menu(flavors, toping, sprinkles): + """Fill out ice cream menu. + + :param flavors: tuple - ice cream flavors. + :param toping: tuple - ice cream toppings. + :param sprinkles: tuple - ice cream sprinkles. + :return: list - ice cream menu filled out. + """ + return list(itertools.zip_longest(flavors, toping, sprinkles, fillvalue="None")) + + \ No newline at end of file diff --git a/exercises/concept/ice-cream-stand/ice_cream_stand.py b/exercises/concept/ice-cream-stand/ice_cream_stand.py new file mode 100644 index 0000000000..cdbdfef829 --- /dev/null +++ b/exercises/concept/ice-cream-stand/ice_cream_stand.py @@ -0,0 +1,31 @@ +"""Functions for ice cream stand.""" + +def ice_cream_combinations(flavors, scoops): + """Return a tuple of all possible combinations without repetition. + + :param flavors: list - arbitrary number of flavors. + :param scoops: int - number of scoops. + :return: tuple - tuple of all possible combinations. + """ + pass + + +def sprinkles(ice_creams, selector): + """Get the ice cream with sprinkles. + + :param ice_creams: list - ice_cream_orders. + :param selector: list - which ice creams that needs sprinkles. + :return: list - ice creams that needs sprinkles. + """ + pass + + +def fill_out_ice_cream_menu(flavors, toping, sprinkles): + """Fill out ice cream menu. + + :param flavors: tuple - ice cream flavors. + :param toping: tuple - ice cream toppings. + :param sprinkles: tuple - ice cream sprinkles. + :return: list[tuple] - ice cream menu filled out. + """ + pass \ No newline at end of file diff --git a/exercises/concept/ice-cream-stand/ice_cream_stand_test.py b/exercises/concept/ice-cream-stand/ice_cream_stand_test.py new file mode 100644 index 0000000000..db750943b5 --- /dev/null +++ b/exercises/concept/ice-cream-stand/ice_cream_stand_test.py @@ -0,0 +1,82 @@ +import unittest +import pytest +from ice_cream_stand import (ice_cream_combinations, sprinkles, fill_out_ice_cream_menu) + + +class IceCreamStandTest(unittest.TestCase): + + @pytest.mark.task(taskno=1) + def test_ice_cream_combinations(self): + input_data = [(['vanilla', 'chocolate', 'strawberry'], 2), + (['vanilla', 'chocolate', 'strawberry'], 1), + (['vanilla', 'chocolate', 'strawberry'], 3), + (['vanilla', 'chocolate', 'strawberry', 'mint'], 2), + (['vanilla', 'chocolate', 'strawberry', 'mint', 'raspberry', 'blueberry'], 4) + ] + output_data = [(('vanilla', 'chocolate'), ('vanilla', 'strawberry'), ('chocolate', 'strawberry')), + (('vanilla',), ('chocolate',), ('strawberry',)), + (('vanilla', 'chocolate', 'strawberry'),), + (('vanilla', 'chocolate'), ('vanilla', 'strawberry'), ('vanilla', 'mint'), ('chocolate', 'strawberry'), ('chocolate', 'mint'), ('strawberry', 'mint')), + ( + ('vanilla', 'chocolate', 'strawberry', 'mint'), + ('vanilla', 'chocolate', 'strawberry', 'raspberry'), + ('vanilla', 'chocolate', 'strawberry', 'blueberry'), + ('vanilla', 'chocolate', 'mint', 'raspberry'), + ('vanilla', 'chocolate', 'mint', 'blueberry'), + ('vanilla', 'chocolate', 'raspberry', 'blueberry'), + ('vanilla', 'strawberry', 'mint', 'raspberry'), + ('vanilla', 'strawberry', 'mint', 'blueberry'), + ('vanilla', 'strawberry', 'raspberry', 'blueberry'), + ('vanilla', 'mint', 'raspberry', 'blueberry'), + ('chocolate', 'strawberry', 'mint', 'raspberry'), + ('chocolate', 'strawberry', 'mint', 'blueberry'), + ('chocolate', 'strawberry', 'raspberry', 'blueberry'), + ('chocolate', 'mint', 'raspberry', 'blueberry'), + ('strawberry', 'mint', 'raspberry', 'blueberry') + )] + + for variant, (input_data, output_data) in enumerate(zip(input_data, output_data), start=1): + with self.subTest(f'variation #{variant}', input_data=input_data, output_data=output_data): + error_msg=f'Expected: {output_data} but got different result' + self.assertEqual(ice_cream_combinations(input_data[0], input_data[1]), output_data, msg=error_msg) + + + @pytest.mark.task(taskno=2) + def test_sprinkles(self): + input_data = [(['ice_cream_1', 'ice_cream_2', 'ice_cream_3'], [0, 1, 0]), + (['ice_cream_1', 'ice_cream_2', 'ice_cream_3'], [1, 0, 1]), + (['ice_cream_1', 'ice_cream_2', 'ice_cream_3', 'ice_cream_4', 'ice_cream_5'], [1, 1, 0, 0, 1]), + (['ice_cream_1', 'ice_cream_2', 'ice_cream_3', 'ice_cream_4', 'ice_cream_5'], [0, 0, 0, 0, 0]), + (['ice_cream_1', 'ice_cream_2', 'ice_cream_3', 'ice_cream_4', 'ice_cream_5', 'ice_cream_6', 'ice_cream_7'], [1, 1, 1, 1, 1, 0, 0]), + ] + output_data = [['ice_cream_2'], + ['ice_cream_1', 'ice_cream_3'], + ['ice_cream_1', 'ice_cream_2', 'ice_cream_5'], + [], + ['ice_cream_1', 'ice_cream_2', 'ice_cream_3', 'ice_cream_4', 'ice_cream_5'] + ] + + for variant, (input_data, output_data) in enumerate(zip(input_data, output_data), start=1): + with self.subTest(f'variation #{variant}', input_data=input_data, output_data=output_data): + error_msg=f'Expected: {output_data} but got different result' + self.assertEqual(sprinkles(input_data[0], input_data[1]), output_data, msg=error_msg) + + + @pytest.mark.task(taskno=3) + def test_fill_out_ice_cream_menu(self): + input_data = ((('vanilla', 'chocolate', 'strawberry'), ('cherry', 'raspberry'), ('licorice',)), + (('strawberry', 'chocolate', 'vanilla'), ('cherry',), ('licorice', 'caramel')), + (('chocolate', 'vanilla', 'strawberry'), ('cherry', 'raspberry', 'blueberry'), ('licorice', 'caramel', 'chocolate')), + (('chocolate', 'vanilla', 'strawberry'), (), ()), + (('strawberry', 'choclate', 'mint', 'vanilla'),('cherry', 'raspberry', 'blueberry'), ('licorice', 'caramel'))) + output_data = [[('vanilla', 'cherry', 'licorice'), ('chocolate', 'raspberry', 'None'), ('strawberry', 'None', 'None')], + [('strawberry', 'cherry', 'licorice'), ('chocolate', 'None', 'caramel'), ('vanilla', 'None', 'None')], + [('chocolate', 'cherry', 'licorice'), ('vanilla', 'raspberry', 'caramel'), ('strawberry', 'blueberry', 'chocolate')], + [('chocolate', 'None', 'None'), ('vanilla', 'None', 'None'), ('strawberry', 'None', 'None')], + [('strawberry', 'cherry', 'licorice'), ('choclate', 'raspberry', 'caramel'), ('mint', 'blueberry', 'None'), ('vanilla', 'None', 'None')] + ] + + for variant, (input_data, output_data) in enumerate(zip(input_data, output_data), start=1): + with self.subTest(f'variation #{variant}', input_data=input_data, output_data=output_data): + error_msg=f'Expected: {output_data} but got different result' + self.assertEqual(fill_out_ice_cream_menu(input_data[0], input_data[1], input_data[2]), output_data, msg=error_msg)